为什么要有锁升级?
在JDK 1.6及之前的版本中,synchronized锁是通过对象内部的监视器锁来实现的。当一个线程请求锁时,如果该对象没有被锁住,线程会获得锁并继续执行。如果该对象已经被锁住,线程就会进入阻塞状态,直到锁被释放。这种锁的实现方式称为重量级锁,因为获取锁和释放锁都需要在操作系统层面上进行线程的阻塞和唤醒,这些操作会带来很大的开销。
JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。
锁的状态有哪些?
在Java中,锁的状态一共有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁。
ps:在JDK 15中,偏向锁已经被废弃了。
synchronized 用的锁是存在锁对象的对象头的Mark Word中,如下图所示。
锁升级的过程是什么样的?
锁升级的大致过程如下:
无锁升级为偏向锁
一个线程初次执行到synchronized代码块的时候,锁对象变成偏向锁。在偏向锁模式下,锁会偏向于第一个获取它的线程,JVM 会在对象头中记录该线程的 ID 作为偏向锁的持有者,并修改 Mark Word 中锁标识为偏向锁。
执行完同步代码块后,线程并不会主动释放偏向锁。当第二次执行同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,偏向锁几乎没有额外开销,性能极高。
偏向锁升级为轻量级锁
当有其它线程尝试获取已被偏向的锁时,偏向锁会经历撤销过程。
撤销过程在安全点执行,此时JVM会检查锁对象的状态。如果发现持有偏向锁的线程不再活动,将对象头恢复为无锁状态。如果锁确实处于被争夺状态,则会升级为轻量级锁。
当一个线程尝试获取已经被持有的轻量级锁时。JVM会做如下操作:
- JVM会在这个线程的栈帧中创建一个锁记录空间,然后将对象头中的Mark Word复制到这个锁记录中。目的是为了保留对象的原始信息,在锁释放时能够恢复对象头的原始状态。
- 尝试通过CAS操作更新对象头的Mark Word中指向锁记录的指针。如果更新成功,那么这个线程就成功获取了锁。
CAS失败会进入自旋状态,自旋次数达到限制会升级为重量级锁。
轻量级锁升级为重量级锁
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等。
然而此忙等是有限度的。如果锁竞争情况严重,线程的自旋次数达到限制,JVM 会将该对象的锁变成一个重量级锁,并在对象头中记录指向等待队列的指针。
在重量级锁状态下,线程在获取锁失败时会被操作系统挂起,放入到该对象关联的监视器(Monitor)的阻塞队列中,由操作系统进行线程调度,当锁被释放时,操作系统会选择合适的线程将其唤醒并授予锁。
锁可以降级吗?
当前的HotSpot虚拟机实现是不支持锁从重量级状态回退到轻量级或偏向锁状态。
但是有一种特殊情况,会从重量级锁恢复到无锁状态。就是重量级锁的Monitor对象在不再被任何线程持有时,被清理和回收。这个过程可以在Stop-the-World(STW)暂停期间进行,这时所有Java线程都停在安全点(SafePoint)。JVM会进行:
- 锁状态检查:在STW停顿期间,JVM会检查所有的Monitor对象。
- 确定降级对象:JVM识别出那些没有被任何线程持有的Monitor对象。通常是通过检查Monitor对象的锁计数器或者所有权信息来实现。
- "降级"操作:对于那些确定未被持有的Monitor对象,JVM会进行所谓的“deflation”操作,即清理这些对象的状态,使其不再占用系统资源。在某些情况下,这可能涉及到重置Monitor状态,释放与其相关的系统资源等。此时,锁会恢复到无锁状态。
以上仅针对 HotSpot 虚拟机而言。
锁升级过程中有几处自旋?
synchronized升级过程中有2处自旋过程。
第一处自旋
第一处自旋发生在获取轻量级锁时,当一个线程尝试获取被其他线程持有的轻量级锁时,它会自旋等待锁的持有者释放锁。
在OpenJDK 8中,轻量级锁的自旋默认是开启的,最多自旋15次,每次自旋的时间逐渐延长。如果15次自旋后仍然没有获取到锁,就会升级为重量级锁。
JDK6之后,JVM引入了自适应的自旋机制。该机制可以通过监控轻量级锁自旋等待的情况,动态调整自旋等待的时间。
第二处自旋
第二次自旋发生在获取重量级锁时。即当一个线程尝试获取被其他线程持有的重量级锁时,它会自旋等待锁的持有者释放锁。
在OpenJDK 8中,默认不会开启重量级锁自旋。如果线程在尝试获取重量级锁时,发现该锁已经被其他线程占用,那么线程会直接阻塞,等待锁被释放。如果锁被持有时间很短,我们可以考虑开启重量级锁自旋,避免线程挂起和恢复带来的性能损失。