
一、对象头与标记字的基础结构
要理解偏向锁的工作原理,首先要看 HotSpot 虚拟机中每个对象前部的两个关键部分:Mark Word与Klass Pointer。Klass Pointer 指向方法区中的类元数据,而 Mark Word 则是一段长度与系统位数一致的动态比特位区域,其含义会随着锁状态的变化而改变。在无锁状态和偏向锁状态下,Mark Word 都需要保留对象的哈希码(如果已经计算过)、分代年龄等信息,但偏向锁还会额外占用若干位来存储持有锁的线程 ID 以及一个“是否偏向”的标志位。
以 64 位虚拟机且开启压缩指针为例,Mark Word 依然保持 64 位宽度。其中,偏向锁标志位为 1 且末尾锁标志位为01时,整个对象处于“可偏向”或“已偏向”状态。此时,大约有 54 位空间被用来存放 Java 层Thread对象的唯一标识。这意味着仅通过读取并比对 Mark Word 中这几十位数据,虚拟机就可以判断当前线程是否已经“拥有”该对象的偏向锁。下面的代码片段演示了用伪代码提取 Mark Word 并核对偏向线程的基本思路:
// 伪代码:读取对象标记字判断偏向状态
long mark = unsafe.getLong(obj, MARK_OFFSET);
boolean biased = (mark & BIASED_LOCK_PATTERN) == BIASED_LOCK_PATTERN;
if (biased) {
long threadId = (mark >> THREAD_ID_SHIFT) & THREAD_ID_MASK;
if (threadId == currentThreadId) {
// 偏向命中,直接进入同步块
} else {
// 需要撤销偏向或触发重偏向
}
}这种设计使得首次获取锁的线程在后续执行中完全避开了 CPU 的原子指令(CAS)。CAS 操作在多核环境下会引起总线锁定或缓存锁,从而引入一致性消息流量,如果频繁执行代价相当可观。偏向锁将“每次获取锁”的成本简化为“首次写入 + 后续只读比较”,在特定负载下可以带来直接的性能红利。
二、无竞争场景下的性能红利来源
所谓“无竞争”,是指除了持有偏向的线程之外,再没有其他线程尝试进入同一个同步块。在该前提下,偏向锁避免了两项主要开销:一是每次进入和退出synchronized块时所需的原子指令,二是 HotSpot 解释器或 JIT 编译器在监视器 enter/exit 路径上的簿记工作。对于那些粒度极细但调用频率极高的同步操作,例如线程安全的日期格式化器、基于synchronized的局部计数器等,这种消除可以带来 10% 甚至 50% 以上的吞吐量提升。
考虑一个典型的计数器示例。如果关闭偏向锁(通过-XX:-UseBiasedLocking),每次执行synchronized方法都会走轻量级锁的路径,需要在线程栈帧中创建锁记录、用 CAS 替换 Mark Word、并在退出时恢复。而开启偏向锁后,只有第一次进入时会执行一次 CAS 将线程 ID 写入对象头,此后所有重复调用仅仅是比较 ID 是否一致,连轻量级锁的栈操作都省去了:
public class Counter {
private int value = 0;
// 单线程反复调用
public synchronized void increment() {
value++;
}
}
// 测试:仅一个线程反复获取锁
for (int i = 0; i < 10_000_000; i++) {
counter.increment();
}从底层硬件角度看,轻量级锁即使没有线程争抢,也要发出带有排他语义的 CAS 指令,该指令会迫使处理器执行缓存一致性协议,严重时还会触发写缓冲区冲刷。偏向锁则让后续所有进入都由一条普通的加载指令完成,完全不会产生总线锁定或缓存行颠簸。因此,红利的多少很大程度上取决于“偏向线程持有锁的时间长度”与“不同线程争抢该锁的频次”之间的比值:比值越大,偏向锁的价值越突出。
三、偏向的延迟与撤销机制
HotSpot 并不是在程序启动的瞬间就给所有新对象开启偏向。JVM 预设了一个偏向延迟参数BiasedLockingStartupDelay,默认约 4 秒,目的是避免启动阶段大量类加载、初始化引发的多线程竞争导致偏向锁被频繁撤销。在延迟窗口内创建的对象都处于可偏向但未偏向的状态,等到延迟结束,首个获取锁的线程才会真正将 ID 写入对象头,完成“偏向”。
当另一个线程尝试进入已经偏向的同步块时,发现 Mark Word 中的线程 ID 并非自己,就会触发偏向撤销(revocation)。撤销的过程并不简单:它需要先将持有偏向的线程挂起到安全点(safepoint),遍历其栈帧检查是否仍然持有该锁,再根据结果决定是升级为轻量级锁还是执行重偏向。如果发现原持有线程已经退出同步块,虚拟机会尝试将偏向直接重指向当前线程(重偏向);如果原线程仍处于同步块内部,则必须升级为轻量级锁以保障正确性。下面的伪代码展示了这一决策分支:
// 伪代码:处理偏向锁竞争
if (owningThread == null || !owningThread.isAlive()) {
// 原持有线程已消亡,可以直接重偏向给新线程
mark = setThreadId(mark, currentThreadId);
} else if (owningThread.isSafepointSafe()) {
// 原线程已退出同步块,执行重偏向
rebiasToCurrentThread(obj);
} else {
// 原线程仍持有锁,将偏向锁升级为轻量级锁
revokeBiasedToLightweight(obj);
}当系统中大量对象被一个线程偏向后,又被另一个线程接管,就会触发 JVM 内置的启发式策略:批量重偏向(bulk rebias)和批量撤销(bulk revoke)。虚拟机以类为单位统计偏向撤销次数,一旦超过阈值(由BiasedLockingBulkRebiasThreshold和BiasedLockingBulkRevokeThreshold控制),就会对此后该类的所有新对象直接偏向到新线程,或者干脆彻底禁用该类的偏向锁。这说明偏向锁在“线程亲和性极强”的场景下收益最大,而在“同一对象被多个线程反复轮换使用”的模式下反而可能因撤销开销而拖累性能。
四、实践中的取舍与调优
在真实的分布式系统或微服务应用中,每个请求通常由不同的 Worker 线程处理,并可能触及一些共享的带同步的组件(例如连接池、缓存容器)。这种情况下,偏向锁往往弊大于利,因为频繁的撤销和升级会消耗额外的 CPU 时间,甚至引发安全点停顿。因而许多高并发 Web 框架、数据库连接池内部都会显式地通过 JVM 参数关闭偏向锁。反之,在单机计算、批处理任务、GUI 事件线程或者明确只由固定线程操作的场景中,开启偏向锁则能稳定降低同步开销。
如果我们想观测偏向锁的实际效果,可以借助以下 JVM 参数进行诊断和调优:
-XX:+PrintBiasedLockingStatistics:输出偏向锁的计数和撤销统计,帮助判断偏向是否频繁失效。-XX:BiasedLockingStartupDelay=0:将偏向延迟设置为 0,适合短生命周期程序或压测时提前让偏向生效。-XX:-UseBiasedLocking:在已知存在大量线程竞争的情况下,直接关闭偏向锁,避免撤销开销。-XX:BiasedLockingBulkRebiasThreshold和-XX:BiasedLockingBulkRevokeThreshold:调整批量重偏向和撤销的阈值,适用于对延迟极端敏感的特定类实例。
从软件架构的角度看,偏向锁只是一种“让不可避免的同步付出更小代价”的优化手段,而不是鼓励无节制地使用synchronized。在可行的情况下,通过无锁数据结构、不可变对象或线程封闭等方式彻底消除共享可变状态,始终是最可靠的性能保证。只有当设计上确实需要同步,且确认为长期单线程主导的访问模式时,偏向锁这种在对象头中“悄然粘附”线程 ID 的设计,才能帮助应用安静地收获它所承诺的性能红利。
biased_locking对象头无竞争锁优化修改时间:2026-08-02 22:11:02