自旋(Spinning)、适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等。

1.1 自旋锁与自适应性自旋

互斥同步对性能最大的影响是阻塞的实现,挂起和恢复线程的操作都需要转入内核态完成,这些操作给并发性能带来很大的压力。如果获取共享数据锁时仅需要等待很短的时间,为了很短的时间去挂起和恢复线程并不值得。如果处理器有一个以上的处理器,我们可以线程让请求锁的线程忙循环,不放弃处理器的执行时间,这项技术就是所谓的自旋锁。
自适应自旋锁意味着自旋的时间不再固定,根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

1.2 锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为是线程私有的,同步加锁自然无需执行。

1.3 锁粗化

如果一系列的连续操作都对同一个对象反复加锁解锁,甚至加锁操作出现在循环体中。如果虚拟机探测到有这样一串零碎的操作都对一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,这样只加锁一次就可以。

1.4 偏向锁

偏向锁目的是消除数据在无竞争情况下的同步原语。
使用场景:锁不仅不存在多线程竞争,而且总是由同一线程多次获得。

1.4.1 偏向锁原理

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步时,虚拟机都可以不再进行任何同步操作,只需简单测试对象头的Mark Word是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得锁。如果测试失败,则需要在测试一下Mark Word中偏向锁的标识是否设置成”01”;如果没有设置,则使用CAS竞争锁,如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,继续执行同步块;如果竞争失败,则进行偏向锁撤销。如果设置了则尝试使用CAS将对象头的偏向锁指向当前线程。

1.4.2 偏向锁撤销(Revoke Bias)

当有另外一个线程尝试去获取这个锁时,偏向模式结束。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。在安全点时它首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果不处于活动状态,则将对象头设置成无锁状态;如果线程还活着,拥有偏向锁的栈会被执行,在栈中创建锁记录,遍历偏向对象头(MarkWord)中的锁记录,将对象头中的锁记录拷贝到栈的锁记录中,修改锁标识为轻量级锁,唤醒持有原持有偏向锁的线程,从安全点继续执行。

偏向锁初始化及撤销流程图:
bias-expand
图摘自Java并发编程艺术 2.2小节

上图只说明了一种情况,即安全点时检测线程是否存活,上图是线程已经执行完同步块,线程已经处于未活动状态(执行完了),所以撤销偏向锁是将锁置为无锁状态。如果代码未执行完成处于活动状态,则升级为轻量级所。

1.5 轻量级锁

轻量级锁是JDK1.6 中加入的新型锁机制,轻量级是相对于使用操作系统互斥量来实现传统锁而言,因此传统锁机制被称为重量级锁。轻量级锁本意指在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

HotSpot虚拟机的对象头(Object Header)分为两部分,第一用于存储自身的运行时数据,这部分数据的长度在32位和64位的虚拟机分别是32个和64个bits,官网称它为”Mark Word”。另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还有一个额外的部分用于存储数组长度。
mark word
表摘自Java并发编程艺术 2.2小节
对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,根据对象的状态复用自己的存储空间。
HotSpot 虚拟机对象头Mark Word不同状态下存储内容表:
mark word
表摘自深入理解Java虚拟机-JVM高级特性与实践 13.3小节
上表中存储内容列是Mark Word中除标志位外,其他的30bit空间的存储内容,状态列是指不同的锁状态,如:轻量级锁,重量级锁等等。具体内容存储占位如下图所示
mark word status
表摘自Java并发编程艺术 2.2小节
轻量级锁的执行过程:在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word拷贝,如图:
markword-cas-before
图摘自深入理解Java虚拟机-JVM高级特性与实践 13.3小节
然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新成功了,那么该线程就拥有了该对象的锁,并且对象Mark Word的锁标志位将转为“00”,即表示该对象处于轻量级锁定状态。
markword-cas-after
图摘自深入理解Java虚拟机-JVM高级特性与实践 13.3小节
如果更新失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了该对象的锁,可直接进入同步代码块继续执行,否则说明这个锁对象被其他线程抢占了;如果否,则当前线程进入自旋,等待锁。如果此时有两条以上(包括两条)的线程(一条处于自旋,一条又来竞争)争用同一个锁,轻量级锁不再有效要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁的指针,后面等待的线程也要进入阻塞状态。
轻量级锁初始化及膨胀流程图:
lightweight
图摘自Java并发编程艺术 2.2小节
解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍指向线程的锁记录,就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果替换成功,同步完成。替换失败,有其他线程尝试获取该锁,此时锁已经膨胀,就要在释放锁的同时,唤醒被挂起的线程。

1.6 重量级锁

1.6.1 原理

重量级锁的实现和JVM的对象监视器相关(Object-Monitor)。在Java虚拟机中每一个对象都和一个Monitor相关。ObjectMonitor的相关数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

轻量级锁升级为重量级锁时,会修改MarkWord中的锁标志为11,在MarkWord中存储指向Monitor的指针。多个线程访问同一个对象时,只有一个线程会进入_owner,其他的都在_EntryList等待,如果线程在持有_owner时,选择wait,则线程进入_WaitSet。_EntryList、_WaitSet中的线程在持有_owner的线程释放后,都会竞争_owner。

1.7 轻量级锁、偏向锁、重量级锁对比

偏向锁初始化及膨胀流程图:
lock
图摘自Java并发编程艺术 2.2小节

参考资料

深入理解Java虚拟机-JVM高级特性与实践
Java并发编程实践
Java并发编程的艺术
啃碎并发(七):深入分析Synchronized原理-掘金
Java偏向锁是如何撤销的?
Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing
biasedlocking
Evaluating and improving biased locking in the HotSpot virtual machine