0%

JVM锁优化

自旋锁

互斥同步对性能最大的影响在于阻塞的实现,,因为挂起线程和恢复线程都需要转入到内核态进行完成。通过不放弃处理器的执行时间,让线程忙循坏,看看持有锁的线程能否很快就释放锁,就是自旋锁。

自旋锁虽然避免了线程切换的开销,但是需要占用处理器时间,所以如果锁被占用的时间很短,自旋等待的效果会比较好,反之只能白白的消耗处理器资源,带来性能的浪费。因此自选等待的时间必须要有一定的限度,如果超过自旋的限定次数仍然没有成功获得锁,那么就应当用传统的方式来挂起线程。自旋次数的默认值为10次,可以使用-XX:PreBlockSpin参数来自行修改。

在JDK6以后加入了对于自旋锁的优化,引入了自适应的自旋锁。自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者状态来决定。

锁消除

锁消除指的是虚拟机即时编译器在运行时,对一些代码有球同步但是被检测到不存在共享数据的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。

锁粗化

在通常情况下,同步块的作用范围应该限制的尽量小,这样即使存在锁竞争,等待锁的线程也能尽可能地拿到锁。

但是如果一系列的操作对同一个对象反复进行加锁和解锁,那么即使没有线程竞争,频繁的进行互斥行为也会导致不必要的性能损耗。如果虚拟机探测到有这样一连串的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。

轻量级锁

轻量级锁的设计初衷在于在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

虚拟机的对象头分为两个部分,其中第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄等,称为Mark Word,是实现轻量级锁和偏向锁的关键。

img

在代码即将进入同步块的时候,如果此时同步对象没有被锁定,那么虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word的拷贝。然后虚拟机使用CAS操作尝试把对象的Mark Word更新为指向Mark Record的指针。如果更新成功,即代表该线程拥有了这个对象的锁,并且对象的Mark Word的锁标志位转变为00,代表该线程处于轻量级锁定状态。如果更新失败了,那么说明至少有一条线程竞争获取该对象的锁。虚拟机会首先检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,直接进入同步块继续执行就行了,否则膨胀为重量级锁,锁标志位变为10,此时Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也必须进入阻塞状态。

解锁过程同样通过CAS进行完成,如果对象的Mark Word仍然指向线程的锁记录,那么就用CAS操作将当前的Mark Word和线程中复制的Mark Word替换回来。假如能够成功替换,那么整个同步过程就顺利完成了。如果替换失败,那么说明其他线程尝试获取过该锁,需要在释放锁的同时唤醒被挂起的线程。

偏向锁

偏向锁的目的在于消除数据在无竞争的情况下的同步原语,进一步提升程序的运行性能。偏向锁的偏表示这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程所获取,则持有偏向锁的线程就永远不需要进行同步。

当锁对象第一次被线程获取的时候,虚拟机会将对象头的标志位设置为01,把偏向模式设置为1,表示进入偏向模式,同时使用CAS操作把获取到这个锁的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁的同步块时,虚拟机都可以不再进行任何同步操作。

一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向,撤销后标志位恢复到未锁定或者轻量级锁定的状态,后续的同步状态就按照轻量级锁去执行。