Java并发——偏向锁

在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需要执行以下步骤:

  1. 到达一个安全点(所有线程暂停执行)。

  2. 遍历当前线程的栈帧,查找锁记录,如果发现持有偏向锁的线程已经退出同步块(即锁记录已释放),则撤销偏向锁,将对象头恢复为无锁状态(或直接升级为轻量级锁)。

  3. 如果持有偏向锁的线程仍然存活且仍然需要该锁,则将偏向锁升级为轻量级锁,让两个线程通过CAS竞争。

  4. 恢复所有线程。

由于安全点会导致STW(Stop The World),因此频繁的偏向锁撤销会对系统性能产生负面影响,尤其是在高并发场景下。

五、偏向锁的设计初衷与适用场景

偏向锁的设计初衷是为了优化无竞争或极低竞争的场景。典型场景包括:

  • 单线程反复执行同步块(例如在启动阶段、单线程应用)。

  • 使用了Vector、Hashtable等遗留同步集合,但实际并未被多线程并发访问。

  • 某些框架或库内部使用的锁,在大部分情况下只有一个线程持有。

在这些场景中,偏向锁可以消除所有同步开销,提升吞吐量。

六、偏向锁的缺点与废弃原因

尽管偏向锁在特定场景下表现优秀,但现代Java应用逐渐暴露出其弊端:

  1. 撤销成本高昂

    在高并发应用中,锁往往会被多个线程争用,导致频繁的偏向锁撤销。撤销需要全局安全点,这会引起STW暂停,增加延迟。对于延迟敏感的服务(如微服务、实时计算),这种开销不可接受。

  2. 现代硬件对CAS的优化

    随着硬件发展,CAS操作的成本已经大幅降低。轻量级锁(通过CAS自旋)的效率在多数情况下已经足够好,偏向锁带来的额外收益变小。

  3. 应用部署环境的复杂化

    偏向锁的优化效果依赖于应用是否在"无竞争"环境中运行。而如今很多应用运行在容器、云平台中,线程数多,竞争模式复杂,偏向锁的默认开启反而可能带来性能倒退。

  4. 维护成本

    偏向锁的实现涉及复杂的撤销逻辑,增加了JVM代码的复杂度和维护难度。

基于以上原因,Java 15 中默认禁用了偏向锁,通过JEP 374将其废弃。在Java 18中,偏向锁被彻底移除(JEP 376),相关代码被清理。如果仍想使用,需通过 -XX:+UseBiasedLocking 手动开启,但未来版本不再支持。

七、性能测试:偏向锁 vs 轻量级锁

为了直观感受偏向锁的影响,可以运行一个简单的测试:创建一个同步块,让多个线程交替竞争锁。在开启偏向锁(JDK 8默认开启)和关闭偏向锁(-XX:-UseBiasedLocking)的情况下,观察吞吐量和延迟。

结果往往显示:

  • 在单线程或低竞争场景,开启偏向锁的吞吐量略高。

  • 在高竞争场景,关闭偏向锁后的性能更稳定,且延迟更低。

这也印证了现代应用更适合使用轻量级锁或更高级的并发工具。

八、总结

偏向锁是HotSpot JVM为优化synchronized而设计的一把"双刃剑"。它巧妙利用线程局部性,减少了无竞争下的同步开销,但随着并发程度的提高和硬件技术的进步,其缺点逐渐暴露,最终被移出默认选项。

作为开发者,我们不必为偏向锁的移除感到惋惜,而应更关注如何选择合适的锁策略:

  • 对于竞争较低的场景,synchronized的轻量级锁已经足够高效。

  • 对于高竞争场景,建议使用java.util.concurrent包中的显式锁(如ReentrantLock)或更高级的同步工具。

相关推荐
moxiaoran57532 小时前
使用springboot+flowable实现一个简单的订单审批工作流
java·spring boot·后端
牧天白衣.2 小时前
07-常用API
java
Meepo_haha2 小时前
Tomcat闪退问题以及解决原因(三种闪退原因有解决办法)
java·tomcat·firefox
兑生2 小时前
【灵神题单·贪心】3010. 将数组分成最小总代价的子数组 I | Java
java·开发语言·算法
小堃学编程2 小时前
【项目实战】基于protobuf的发布订阅式消息队列(1)—— 准备工作
java·大数据·开发语言
吴声子夜歌2 小时前
JavaScript——数组
java·javascript·算法
稻草猫.2 小时前
MyBatis-Plus高效开发全攻略
java·数据库·后端·spring·java-ee·mybatis·mybatis-plus
季明洵2 小时前
回溯介绍及实战
java·数据结构·算法·leetcode·回溯
人道领域2 小时前
Day | 09 【苍穹外卖:订单售后业务】
java·数据库·后端