Java同步锁类型

Published: by Creative Commons Licence

  • Tags:

前言

java中syncronized关键字用于获取对象上的monitor锁。而monitor锁基于底层的操作系统的互斥锁实现,因此每次加锁和解锁需要程序从用户态切换到内核态,成本相对较高。这也导致了syncronzied效率比java中其它互斥锁低很多。JDK中对syncronized做了比较多的优化,以避免对monitor的操作,在JDK1.6后为了优化syncronzied引入了偏向锁轻量锁

锁升级

java中的对象锁分为无锁,偏向锁,轻量锁,重量锁。锁允许进行升级,但不允许降级,无锁可以升级为偏向锁,偏向锁可以升级为轻量锁,轻量锁可以升级重量锁。

对象地址的前若干个字节记录了对象的信息,包含Mark Word(存储对象的哈希码和锁信息),Class Metadata Address(存储对象类型的地址),Array length(仅数组拥有,记录数组长度)。

对象头部的32bit(64位系统位前64bit)记录了Mark word字段,其中包含了对象锁的信息。其倒数2bit(30,31)表示锁类型,00表示轻量锁,10表示重量锁,01表示偏向锁或无锁(倒数第3bit为1代表偏向锁,为0表示无锁),11表示GC标记。

Mark Word State
unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 No lock
thread:54 |epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 Biased
ptr_to_lock_record:62| lock:2 Lightweight Locked
ptr_to_heavyweight_monitor:62 | lock:2 Heavyweight Locked
| lock:2 Marked for GC

偏向锁

由于轻量级锁的获取需要多次CAS(compare and set)原子指令,而偏向锁仅需要一次CAS原子指令,因此在竞争较弱的情况下偏向锁理论上拥有比轻量锁更优秀的性能。

下面描述偏向锁的获取过程:

如果Mark word为无锁状态,则将锁状态改变为偏向锁状态,然后使用CAS操作将当前线程ID记录在Mark word中。之后该线程再次进入同步块,那么可以直接检测Mark word,如果即处于偏向锁,且记录的线程ID为自己,那么可以直接获得锁而不需要CAS操作。但是如果使用CAS将原来无锁时的信息替换为自己线程ID时竞争失败,那么当到达全局安全点(safepoint)时获得偏向锁的线程将被挂起,并将偏向锁升级为轻量级锁。

持有偏向锁的线程不会主动释放锁(即使离开同步块),只有当其他线程竞争锁时才会撤销。

如果一个对象仅被一个线程所访问,那么偏向锁将会大幅优化线程进入同步块的时间。

轻量锁

重量级锁是悲观锁,它认为总是有不止一个线程要竞争锁,所以它处理共享数据时,不管当前系统是否真的有线程在竞争,总是会使用互斥量来处理同步。

而轻量锁则是一种乐观锁,它认为锁存在竞争的情况较少,即使存在竞争,锁的持有者也会很快脱离临界区并释放锁。因此它选择使用多次CAS操作来获得锁,这也就能减少互斥量带来的用户态和内核态切换带来的高昂费用。

下面描述轻量锁的获取过程:

虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储Mark word的拷贝,名为Displaced Mark Word。之后将对象头的Mark Word拷贝到锁记录中。拷贝成功后,虚拟机使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,同时将锁记录中的owner指针指向对象地址。如果CAS竞争成功,则线程获得锁并进入同步块。如果竞争失败,则首先检查Mark Word是否指向当前线程的栈帧,如果时就说明该锁当前线程已经持有了(重入),那么直接进入同步块。如果不指向自己的栈帧,则说明不止一个线程在竞争锁,轻量级锁膨胀为重量级锁,而Mark Word中存储的就是指向重量级锁(互斥量)的指针。

而轻量级锁的释放则是用对象Mark Word指向锁记录中的Displaced Mark Word利用CAS替换对象的Mark Word。如果替换失败,则表示锁已经被升级为重量锁,那么在释放锁的同时,需要唤醒被挂起的线程。

自旋锁

线程在获得重量锁的时候,会先通过自旋的方式来获取重量锁。但是自旋需要消耗CPU资源,因此自旋是有次数限制的。JDK内部采用了适应性自旋,即通过上一次自旋取该锁时是否成功决定下一次自旋取该锁的自旋次数上限,如果成功则增加上限,失败则减少上限。

锁粗化

当在代码中连续尝试获得相同锁,那么这些取锁的过程将会被合并为一次。这由虚拟机自行优化。

锁消除

利用代码逃逸技术,如果判断某个对象对应的数据不会逃逸出当前线程,那么可以仅当前线程可以获取该对象的锁,这样可以直接跳过对该对象的加锁。