为了解决并发操作时的数据一致性问题,java提供锁机制,通过互斥同步或非阻塞同步来保证线程安全。但只要是同步,就会对程序的执行效率产生影响。从JDK1.6开始,HotSpot虚拟机实现了各种锁优化技术,如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。这些技术使得线程之间可以更高效地共享数据,解决竞争问题,从而提高程序执行效率。
下面介绍这五种锁优化技术原理
一、自旋锁与自适应自旋
引出问题:
在传统的互斥同步中,得到锁的线程进入同步代码,没有得到锁的线程则只能线程挂起,进入阻塞状态,而挂起线程和恢复线程都需要转到内核态由操作系统来完成,如果执行线程等待锁的时间很短,那么线程挂起和线程恢复就属于性能浪费。
自旋锁:
自旋锁就用来解决上述问题。如果两个线程同时竞争一个锁,我们可以让没有竞争到的线程“稍等一下”,但并不放弃处理器的执行时间,也就是不挂起,看看持有锁的线程会不会很快地释放锁。为了让线程等待,我们让线程执行一个忙循环,也就是自旋,这项技术就是“自旋锁”。
优缺点:
自旋锁在锁被占用时间很短的情况下效果非常好,但是如果锁被占用的时间较长,那么自旋的线程会占用处理器资源,造成性能上的浪费。jdk1.6默认自旋是10次,用户可以使用参数-XX:PreBlockSpin来更改。
自适应自旋:
自适应自旋是对自旋锁的改进。自适应自旋以为着自旋次数不再固定,而是根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。也就是通过之前线程的经验得知获得这个锁的难易程度。以决定少量自旋还是多次自旋,甚至直接挂起。
二、锁消除
引出问题:
java程序中同步代码非常的普遍,有可能同步代码中并没有会产生锁竞争的共享变量,此时同步就白白消耗了性能。
锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
举个例子:
public String concatString(String s1,String s2,String s3){
return s1+s2+s3;
}
这段代码看起来没有同步,实际上在jdk1.5之前,它会被转化成StringBuffer对象的连续appen()操作,即:
public String concatString(String s1,String s2,String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
StringBuffer对象的appen()方法是同步方法,但是虚拟机观察对象sb,发现sb的作用域在concatString()方法内,用户调用concatString()方法永远不会存在共享变量的竞争,所以即时编译器将这个锁消除掉了。
三、锁粗化
引出问题:
通常情况下,我们加锁的代码以你该当越短越越好,这样可以减少串行执行的时间,提升执行效率。但是如果一系列的连续操作都对同一个对象反复加锁解锁,那么频繁的同步互斥操作就会影响性能。
锁粗化:
比如上一节中的连续append()方法的情况,都对同一个对象加锁,如果虚拟机探测到有这样一连串的操作都对同一个对象加锁解锁,将会加锁同步的范围粗化到一连串操作的外部,讲多次加锁解锁转化为一次加锁解锁。这就是锁粗化。
四、轻量级锁
轻量级锁是使用CAS操作优化有同步但无竞争的情况。
原理:
要了解轻量级锁的原理,先了解HotSpot虚拟机的对象头的内存布局。
HotSpot虚拟机的对象头包含两部分信息,一部分用于存储自身运行时数据,如hash码、GC分代年龄等,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。另一部分用于存储指向方法区对象类型数据的指针。
Mark Word在32位和64位虚拟机中分别占32bit长度和64bit长度。32位虚拟机中,Mark Word用2bit空间来存储锁标志位。存储内容如下:
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象GC分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 重量级锁定 |
空,不需要记录信息 | 11 | GC标记 |
偏向县城id、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
在代码进入同步块的时候,如果对象没有被锁定(01),虚拟机在当前线程的栈帧中建立一个名为锁记录的空间,用于存储对象目前Mark Word的拷贝。
然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向所记录的指针。如果成功,那么这个线程拥有了该对象的锁,并将该对象标志位改为00,表示该对象处于轻量级锁定状态;如果更新操作失败了,虚拟机将检查对象的Mark Word是否指向当前线程的栈帧,若是,则说明当前线程拥有这个对象的锁,线程直接进入同步块执行,若否,说明这个锁已经被其他线程抢占了。此时多个线程争用同一个锁,轻量级锁不再有效,要膨胀为重量级锁,锁标志变为10。
适用情况:
大多数情况下,整个同步周期内是不存在竞争的,此时轻量级锁适用CAS操作避免了适用互斥量的开销。但在存在竞争的情况下,轻量级锁除了互斥量的开销,还有CAS操作,性能会比传统重量级锁更差。
五、偏向锁
偏向锁的意思是这个锁会偏向于第一次获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程永远不需要进行同步。
原理:
当锁对象第一次被线程获取的时候,虚拟机会将对象头的标志位设为01,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象Mark Word之中。如果CAS成功,持有偏向锁的线程以后每次进入这个对象锁相关的同步块将不再同步。
当有另外一个线程去尝试获取这个锁时,偏向模式宣告结束。根据目前是否处于锁定状态,撤销偏向后恢复到轻量级锁(00)或为锁定(01)状态。
适用情况:
和轻量级锁相似,偏向锁可以提高有同步但无竞争的情况,偏向锁比轻量级锁优化得更彻底。如果程序中大多数的锁总是被多个不同的线程争抢,那么偏向模式就是多余的。
相关虚拟机参数
启用偏向锁:-XX:+UseBiasedLocking
禁用偏向锁:-XX:-UseBiasedLocking
虚拟机实现了多种锁优化技术,但是都不能直接解决所有的性能问题。必须针对不同场景,选择合适的锁优化技术,提高程序性能。