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)或更高级的同步工具。

相关推荐
Kurisu5751 分钟前
深度拆解:从 Linux 内核 Namespace 与 Cgroups 洞察容器技术的底层本质
java·linux·运维
罗超驿2 分钟前
11.LeetCode 1004. 最大连续1的个数 III | 滑动窗口解法详解(Java)
java·算法·leetcode
努力发光的程序员4 分钟前
面试官与程序员谢飞机的3轮Java大厂面试问答实录:涵盖Spring Boot、微服务与数据库技术
java·jvm·spring boot·redis·面试·hibernate·microservices
橙淮5 分钟前
并发编程(四)
java·jvm
z落落9 分钟前
C# Stack栈 / Queue队列+所有集合 终极一页汇总(全覆盖、零遗漏)
java·开发语言·c#
Halo_tjn24 分钟前
NIO 技术的使用
java·开发语言·nio
砍材农夫24 分钟前
物联网 基于netty核心实战-安全tls
java·开发语言·前端·物联网·安全
Python+9926 分钟前
C++ 内存模型 & 底层原理
java·jvm·c++
兰令水29 分钟前
2026.5.30休息一天
java
公众号-老炮说Java29 分钟前
Spring AI Alibaba 硬核实战:Token 原理 → RAG → 多智能体,一篇通
java·人工智能·后端·spring