自旋锁
【Java 基础 21】Java语言中的线程安全和线程安全的实现方式中提到互斥同步主要面临的问题就是线程阻塞和唤醒带来的性能开销,挂起线程和恢复线程的操作都需要转入内核态完成,这对Java虚拟机的并发性能带来很大的压力,当共享数据的锁定状态只持续很短的一段时间,将其他线程挂起和恢复并不值得,因此虚拟机开发团队设计线程没有获取到锁不立即挂起,而是执行一个忙循环(自旋)等待获取锁。
JDK 6之后默认开始自旋锁,可以使用-XX:UsingSpinning参数设置开启/关闭。
自旋等待虽然避免了线程切换的开销,但是它占用处理器的执行时间,如果锁占用时间短,自旋等待的效果就会非常好,但是如果锁的占用时间长,自旋只会浪费处理器资源,因此自旋需要有一定的限度,如果超出限定次数还是没有获取锁,则将线程挂起避免长时间自旋等待。
自旋次数默认10次,可以使用-XX:PreBlockSpin更改自旋次数。
自适应自旋锁
JDK 6对自旋锁的优化引入自适应的自旋。自适应意味着自旋时间不再固定,而是由前一次在同一个锁上的自旋时间以锁的拥有者的状态来决定。
锁消除
虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。
锁消除的主要判定依据来源于逃逸分析的数据支持【深入理解Java虚拟机 第十一章】后端编译与优化之-逃逸分析,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然无须再进行。
锁粗化
如果虚拟机探测到一串零碎的对象都是对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
原则上程序员再编写代码的时候为了使需要同步的操作数量尽可能少,希望将锁细粒化,将同步块的作用范围缩小到共享数据的实际作用域。
轻量级锁
轻量级锁的设计初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
要理解轻量级锁的实现,需要了解HotSpot虚拟机的Mark Word对象头,对象头在32位虚拟机和64位虚拟机中分别占用32个和64个比特。
在代码即将进入到同步块的时候, 如果该对象没有被锁定(锁标志位为“01”),虚拟机先会在栈中创建一个锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(我一开时看到上面那个图就在想轻量级或者重量级锁定后,分代年龄的记录咋办,原来是拷贝保存起来了),然后,虚拟机使用CAS操作(对应字节码指令cmpxche)将对象Mark Word更新为指向栈中锁记录(Lock Record)的指针。
如何这个更新成功,即代表该线程成功获取到这个对象的锁,并将锁标志位更新为“00”,表示该对象处于轻量级锁定状态。
如何这个更新失败,则说明存在其他线程和当前线程竞争获取该对象的锁,虚拟机先检查对象的Mark Word中记录的指向调用栈中所记录(Lock Record)的指针是否指向当前线程的栈帧,如果是,说明当前线程已经获取到该对象的锁,则直接进入同步块,如果不是,则说明对象锁已经其他线程抢占,这时出现锁竞争,轻量级锁膨胀为重量级锁,此时对象Mark Word中存储指向重量级锁(互斥量)的指针,锁标志位更新为“10”,使其他线程阻塞。
偏向锁
偏向锁也是JDK引入的锁优化的手段,它的目的是在没有多线程竞争的前提下,消除同步原语,进一步提高程序运行性能。
轻量级锁是在没有线程竞争的情况下虚拟机使用CAS操作消除同步使用的互斥量,而偏向锁则是进一步的在没有线程竞争的情况下将整个同步消除,连CAS也不需要了。
对象锁第一次被线程获取时,虚拟机会将对象Mark Word的偏向模式标志置“1”,同时CAS将该线程的ID记录到对象Mark Word中,在接下来的执行过程中年,如果没有其他的线程来竞争锁,持有该偏向锁的线程之后每次进入这个锁相关的同步块都不需要进行任何的同步操作。
一旦有其他线程来竞争锁,根据对象锁当前是否处于锁定状态决定对象恢复到未锁定还是轻量级锁定状态。