synchronized
用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
可以先看看系统的互斥实现~
要理解synchoronized原理,我们先讲讲虚拟机(hotspot为例,以下同)对象头部分的布局。对象头分为两部分信息,第一部分用于存储对象自身运行时的数据,如hashcode,GC分代年龄等,总计64位(64位虚拟机,32位虚拟机32位),官方成为mark word,另一部分用于存储指向方法区的对象类型指针。
normal object header
说是normal object header,是相对于加过偏向锁的object(下文再说)。可以看到对象头里面有1bit偏向锁的描述,2bit锁的描述。到底是不是64位,我们可以通过Unsafe(一个很有趣的类)来验证下:
public class Cvolatile { private volatile boolean flag=false; //unsafe不能直接构造,通过反射得到 public static Unsafe getUnsafe() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{ Field field=Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe=(Unsafe) field.get(null); return unsafe; } public static void headerByte() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{ Unsafe unsafe=getUnsafe(); Field field=Cvolatile.class.getDeclaredField("flag"); System.out.println(unsafe.objectFieldOffset(field)); } public static void main(String args[]) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{ headerByte(); } }
以上程序段新建了一个只有boolean变量的类,然后通过unsafe获取了flag的偏移量,控制台输出是8,说明在64位jdk中object header的确是8*8即是64bit。
同步代码块是通过MonitorEnter和MonitorExit专用字节码指令来实现,即在入口区加上MonitorEnter,在出口处加上MonitorExit。那JVM又是怎样维护竞争同一个对象锁的线程呢,以下贴出各大博客都有的流程:
Contention List:所有请求锁的线程将被首先放置到该竞争队列。
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List。
Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set。
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner。
新请求锁的线程将首先被加入到ConetentionList中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现EntryList为空则从ContentionList中移动线程到EntryList。ContentionList是个后进先出的队列,ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。
自旋锁
基于互斥同步的锁对性能最大的影响是阻塞的实现,挂起线程和恢复线程都需要装入内核态中完成。同时,共享数据的锁定状态只会持续一会,为了这段时间挂起和恢复线程都不值得。如果物理机器上有1个以上处理器,能让2个或2个线程同时执行,我们可以让后面请求锁的线程"稍等一会“,但并不放弃处理器的执行时间,看看当前持有锁的线程是否很快就会释放锁,为了让线程等待,我们让线程执行一个忙循环(可以是执行几条空的汇编指令,执行几次循环,就是占着CPU不干事--),这就是所谓的自旋锁。
synchronized中线程在进入ContentionList时首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。
偏向锁
偏向锁就是为提高性能在无竞争的情况下把整个同步都给取消掉。它会偏向于第一个获取它的线程,如果在接下来的执行过程中,没有其他线程竞争这个锁,则持有锁的线程永远不需要再进行同步。当虚拟机启用偏向锁,锁对象第一次被线程获取的时候,虚拟机会将biased lock设置为1即偏向模式,同时使用CAS把获得锁的线程ID写入mark word,如果CAS操作成功,持有锁的线程进入这个锁相关的同步块时候,虚拟机将不进行任何操作。
成功加了偏向锁后的对象头
—————————————————我是分割线—————————————————————
好玩的Unsafe类又出现了,我们可以利用Unsafe来实现简单的sychronized功能
public static void csynchronized(Object syn) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, InterruptedException{ Unsafe unsafe=getUnsafe(); unsafe.monitorEnter(syn); for(int i=0;i<10;i++){ Thread.currentThread().sleep(100l); System.out.println(Thread.currentThread().getId()); } unsafe.monitorExit(syn); } public static void main(String args[]) { final Object syn=new Object(); new Thread(new Runnable(){ public void run() { try { csynchronized(syn); } catch (NoSuchFieldException e) { } catch (SecurityException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InterruptedException e) { } } }).start(); new Thread(new Runnable(){ public void run() { try { csynchronized(syn); } catch (NoSuchFieldException e) { } catch (SecurityException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InterruptedException e) { } } }).start(); }
从控制台结果可以看出线程ID之间并没有穿插,说明实现了synchronized基本功能。当然我们如果只保留csynchronized中的for循环再次运行可以发现线程ID之间有穿插。
———synchronized先到此,接下来将要说说Lock