Java中的偏向锁优化与批量重偏向/批量撤销机制详解
字数 2088 2025-12-14 21:20:50
Java中的偏向锁优化与批量重偏向/批量撤销机制详解
一、问题背景与核心概念
在多线程环境下,synchronized是Java最基础的同步机制。JVM为了提升同步性能,引入了偏向锁(Biased Locking)优化。但在高度竞争的场景下,频繁的锁竞争会降低偏向锁带来的收益,甚至产生负优化。为此,JVM设计了批量重偏向(Bulk Rebias)和批量撤销(Bulk Revoke)机制来动态调整锁策略。
二、偏向锁的核心原理
-
设计目标:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁让这个线程在获得锁之后,后续再进入同步块时无需执行CAS原子指令或操作系统层面的互斥操作,只需简单检查锁对象头的Mark Word中的线程ID是否指向自己。
-
偏向锁的Mark Word结构(以64位JVM为例):
| 锁标志位 (2 bits) | 是否偏向 (1 bit) | 偏向线程ID (54 bits) | 偏向时间戳 (epoch) | 分代年龄 (4 bits) | 对象哈希码 (31 bits) | 未使用 (1 bit) |当锁处于偏向状态时,锁标志位为
01,是否偏向位为1,并记录占用锁的线程ID和epoch值。 -
偏向锁的获取流程:
- 第一次进入同步块时,检查对象头的Mark Word:
- 如果锁标志位为
01且偏向模式为0(无锁状态),则通过CAS将线程ID写入Mark Word,并将偏向模式设为1。 - 如果偏向模式为
1且线程ID指向当前线程,则直接通过检查,无需同步操作。 - 如果偏向模式为
1但线程ID不是当前线程,说明存在竞争,需要进入锁升级流程(偏向锁 → 轻量级锁)。
- 如果锁标志位为
- 第一次进入同步块时,检查对象头的Mark Word:
三、偏向锁的撤销(Revoke)问题
-
撤销触发条件:当另一个线程尝试获取已被偏向的锁时,需要将偏向锁撤销为无锁状态或升级为轻量级锁。
-
撤销的成本:
- 需要在全局安全点(Safepoint) 暂停持有偏向锁的线程。
- 遍历该线程的栈帧,检查是否有该锁对象的锁记录(Lock Record)。
- 若存在,需要修复栈帧和对象头的状态,可能升级为轻量级锁。
-
性能瓶颈:如果某一类锁对象频繁被不同线程争用,每次撤销都触发安全点停顿,开销极大。
四、批量重偏向(Bulk Rebias)机制
-
设计目的:针对同一类的多个对象被多个线程交替使用,但不同线程间没有严重竞争的场景。为了避免频繁撤销偏向锁,JVM允许将一类对象的偏向锁批量重偏向到另一个线程。
-
实现机制:
- 每个类维护一个偏向锁撤销计数器和一个epoch值。
- 当某个类的偏向锁被撤销达到阈值(默认
20次)时,JVM会触发批量重偏向。 - 重偏向过程:
- 遍历所有当前仍存活的该类的实例对象。
- 检查对象的epoch值是否与类的epoch值匹配。
- 若匹配(说明对象仍可偏向),且对象当前被偏向于原线程,则将对象头的epoch更新为类的当前epoch(相当于标记为“可被新线程偏向”)。
- 后续新线程获取该对象的锁时,可以直接CAS将自己的线程ID写入,无需触发完整撤销。
-
示例场景:
- 线程T1创建了N个类A的对象,并对每个对象加锁(偏向T1)。
- 之后线程T2交替使用这些对象,前20次会触发偏向锁撤销(升级为轻量级锁)。
- 在第20次撤销后,JVM触发批量重偏向,后续对象可以直接偏向T2,避免撤销开销。
五、批量撤销(Bulk Revoke)机制
-
设计目的:当某个类的锁对象持续存在多线程竞争,偏向锁已无法带来收益时,JVM会永久禁用该类的偏向锁功能。
-
触发条件:
- 在批量重偏向发生后,该类的偏向锁撤销次数继续累计达到另一个阈值(默认
40次)。 - 或者,在重偏向过程中,发现多数对象已被其他线程占用,不适合继续偏向。
- 在批量重偏向发生后,该类的偏向锁撤销次数继续累计达到另一个阈值(默认
-
执行效果:
- 将该类的偏向标记强制禁用。
- 后续该类的所有新实例对象,其对象头中的偏向标记会直接设为
0(即默认不启用偏向锁)。 - 已存在的实例对象在锁竞争时,会直接升级为轻量级锁,不再尝试偏向。
六、JVM参数与监控
-
相关参数:
# 开启/关闭偏向锁(JDK 15后默认关闭) -XX:+UseBiasedLocking / -XX:-UseBiasedLocking # 偏向锁延迟时间(默认4000ms) -XX:BiasedLockingStartupDelay=4000 # 批量重偏向阈值 -XX:BiasedLockingBulkRebiasThreshold=20 # 批量撤销阈值 -XX:BiasedLockingBulkRevokeThreshold=40 -
监控方法:
- 使用
jstack查看线程锁状态。 - 通过
-XX:+PrintBiasedLockingStatistics(需Debug版JVM)输出偏向锁统计信息。
- 使用
七、偏向锁的现状与演进
- JDK 15+的变化:由于现代应用往往具有高并发特性,偏向锁的维护开销在竞争场景下可能超过其收益。因此JDK 15后默认禁用偏向锁(
-XX:-UseBiasedLocking)。 - 替代方案:轻量级锁(通过CAS自旋)和重量级锁(操作系统互斥量)的组合,在多数场景下已能提供良好性能。
八、总结与最佳实践
- 偏向锁适用场景:明确单线程重复访问的同步块(如某些缓存框架的本地操作)。
- 批量重偏向/撤销的意义:是JVM对锁竞争模式的自适应优化,减少在“偏向错误”时的性能惩罚。
- 现代开发建议:
- 在JDK 15+环境中,无需主动配置偏向锁。
- 若使用旧版本,可在高竞争锁上通过
-XX:-UseBiasedLocking显式关闭。 - 理解机制有助于分析线上锁竞争问题(如通过
-XX:+PrintBiasedLockingStatistics定位批量撤销的类)。