在Java并发编程领域,synchronized关键字一直是实现线程安全的重要手段。为了提高synchronized的性能,HotSpot虚拟机在Java 6中引入了"偏向锁"这一优化技术。然而,随着硬件演进和应用场景的变化,这一曾经的重要优化最终在Java 15中被默认禁用,并在后续版本中逐步移除。本文将深入剖析偏向锁的设计思想、实现原理,以及它为何走向"退役"的历程。
一、从对象头说起
要理解偏向锁,首先需要了解Java对象的内存布局。HotSpot虚拟机中,每个对象都有一个对象头(Object Header),其中包含了运行时数据,如哈希码、GC分代年龄以及锁状态信息。对象头中的Mark Word是关键,它的结构会根据锁状态的不同而动态变化。
在32位JVM中,Mark Word的典型布局如下(简化版):
| 锁状态 | 25位 | 4位 | 1位(偏向) | 2位(锁标志) |
|---|---|---|---|---|
| 无锁 | 对象哈希码 | 分代年龄 | 0 | 01 |
| 偏向锁 | 线程ID | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
| 重量级锁 | 指向重量级锁的指针 | 10 |
锁标志位(2位)和偏向位(1位)共同决定了当前锁的状态。偏向锁的标志位为01,同时偏向位为1。
二、什么是偏向锁?
偏向锁的核心思想是:在一个线程多次获取同一把锁的场景下,让该线程无需进行任何同步操作即可获得锁,从而降低CAS操作带来的开销。
假设一个锁被同一个线程反复获取,比如在单线程环境中或通过循环调用synchronized方法,那么偏向锁允许线程在第一次获得锁后,后续再进入同步块时直接检查对象头中的线程ID是否为自己,如果是,则无需任何CAS操作,直接进入同步块。这可以大大减少无竞争情况下的锁开销。
三、偏向锁的获取与释放
1. 偏向锁的获取
当线程第一次进入同步块时,虚拟机会检查对象头的锁标志位和偏向位。如果是"无锁"状态(标志位01,偏向位0),则通过CAS操作尝试将当前线程ID写入Mark Word的线程ID字段。如果CAS成功,则对象头变为偏向锁状态(标志位01,偏向位1),当前线程获得锁。
之后,当该线程再次进入同一同步块时,只需比较对象头中的线程ID是否与自身相同。若相同,则直接进入,无需任何同步操作。若不同,则说明有其他线程竞争,此时需要撤销偏向锁,升级为轻量级锁。
2. 偏向锁的释放
偏向锁的释放比较特殊:它不会主动释放。只有当其他线程尝试竞争该锁时,偏向锁才会被撤销。撤销偏向锁需要等待全局安全点(SafePoint),暂停持有偏向锁的线程,检查该线程是否存活,再根据情况决定是恢复为无锁状态还是升级为轻量级锁。
因此,偏向锁的"释放"并不是由获得锁的线程主动完成的,而是由竞争触发的被动操作。
四、偏向锁的撤销过程
偏向锁的撤销是偏向锁机制中代价最高的操作。当另一个线程尝试获取已偏向的锁时,JVM需要执行以下步骤:
-
到达一个安全点(所有线程暂停执行)。
-
遍历当前线程的栈帧,查找锁记录,如果发现持有偏向锁的线程已经退出同步块(即锁记录已释放),则撤销偏向锁,将对象头恢复为无锁状态(或直接升级为轻量级锁)。
-
如果持有偏向锁的线程仍然存活且仍然需要该锁,则将偏向锁升级为轻量级锁,让两个线程通过CAS竞争。
-
恢复所有线程。
由于安全点会导致STW(Stop The World),因此频繁的偏向锁撤销会对系统性能产生负面影响,尤其是在高并发场景下。
五、偏向锁的设计初衷与适用场景
偏向锁的设计初衷是为了优化无竞争或极低竞争的场景。典型场景包括:
-
单线程反复执行同步块(例如在启动阶段、单线程应用)。
-
使用了Vector、Hashtable等遗留同步集合,但实际并未被多线程并发访问。
-
某些框架或库内部使用的锁,在大部分情况下只有一个线程持有。
在这些场景中,偏向锁可以消除所有同步开销,提升吞吐量。
六、偏向锁的缺点与废弃原因
尽管偏向锁在特定场景下表现优秀,但现代Java应用逐渐暴露出其弊端:
-
撤销成本高昂
在高并发应用中,锁往往会被多个线程争用,导致频繁的偏向锁撤销。撤销需要全局安全点,这会引起STW暂停,增加延迟。对于延迟敏感的服务(如微服务、实时计算),这种开销不可接受。
-
现代硬件对CAS的优化
随着硬件发展,CAS操作的成本已经大幅降低。轻量级锁(通过CAS自旋)的效率在多数情况下已经足够好,偏向锁带来的额外收益变小。
-
应用部署环境的复杂化
偏向锁的优化效果依赖于应用是否在"无竞争"环境中运行。而如今很多应用运行在容器、云平台中,线程数多,竞争模式复杂,偏向锁的默认开启反而可能带来性能倒退。
-
维护成本
偏向锁的实现涉及复杂的撤销逻辑,增加了JVM代码的复杂度和维护难度。
基于以上原因,Java 15 中默认禁用了偏向锁,通过JEP 374将其废弃。在Java 18中,偏向锁被彻底移除(JEP 376),相关代码被清理。如果仍想使用,需通过 -XX:+UseBiasedLocking 手动开启,但未来版本不再支持。
七、性能测试:偏向锁 vs 轻量级锁
为了直观感受偏向锁的影响,可以运行一个简单的测试:创建一个同步块,让多个线程交替竞争锁。在开启偏向锁(JDK 8默认开启)和关闭偏向锁(-XX:-UseBiasedLocking)的情况下,观察吞吐量和延迟。
结果往往显示:
-
在单线程或低竞争场景,开启偏向锁的吞吐量略高。
-
在高竞争场景,关闭偏向锁后的性能更稳定,且延迟更低。
这也印证了现代应用更适合使用轻量级锁或更高级的并发工具。
八、总结
偏向锁是HotSpot JVM为优化synchronized而设计的一把"双刃剑"。它巧妙利用线程局部性,减少了无竞争下的同步开销,但随着并发程度的提高和硬件技术的进步,其缺点逐渐暴露,最终被移出默认选项。
作为开发者,我们不必为偏向锁的移除感到惋惜,而应更关注如何选择合适的锁策略:
-
对于竞争较低的场景,synchronized的轻量级锁已经足够高效。
-
对于高竞争场景,建议使用
java.util.concurrent包中的显式锁(如ReentrantLock)或更高级的同步工具。