Java 6 引入了偏向锁(Biased Locking),旨在显著降低早期版本中 synchronized锁在无竞争场景下的性能开销;到了 Java 15,偏向锁默认被禁用;最终在 Java 17 中被彻底移除。尽管已退出历史舞台,其“针对常见场景极致优化”的设计思想仍具有重大的借鉴价值。
一、背景
1.1 synchronized 的性能痛点
Java 早期版本(JDK 1.5 之前),synchronized 关键字直接映射到操作系统互斥量(Mutex),每次加锁/解锁都涉及用户态 ↔ 内核态切换,开销巨大(数百纳秒至微秒级)。即使单线程反复进入同步块,也需执行昂贵的原子操作开销。
1.2 锁优化的演进路径
为提升性能,HotSpot JVM 引入了多级锁升级机制:

其中:
- 偏向锁:针对单线程重复加锁场景;
- 轻量级锁:针对多线程交替执行(无竞争) 场景;
- 重量级锁:存在真实竞争时,退回到 OS Mutex。
✅ 中心思想:“大多数锁不存在竞争”(观察表明 >90% 的锁仅被单线程持有)。偏向锁正是为这类场景设计的极致优化。
二、Java 对象内存布局与 Mark Word
要理解偏向锁,必须要理解 Java 对象在内存中的结构,尤其是对象头(Object Header)。
2.1 对象内存布局(64 位 JVM,开启指针压缩)
|
组成 |
大小 |
说明 |
|
Mark Word |
8 字节(64 bits) |
存储哈希码、GC 分代年龄、锁状态等 |
|
Klass Pointer |
4 字节(32 bits) |
指向类元数据(开启 -XX:+UseCompressedOops) |
|
Instance Data |
可变 |
实例字段 |
|
Padding |
可变 |
对齐填充 |
聚焦 Mark Word —— 锁状态的核心载体。
2.2 Mark Word 结构(动态变化)
Mark Word 的内容随锁状态动态重解释(overlaid layout):
|
锁状态 |
Mark Word 布局(64 bits) |
说明 |
|
无锁(Normal) |
unused:25 identity_hashcode:31 unused:1 age:4 biased_lock:1(0) lock:2(01) |
存储 hashCode |
|
匿名偏向(Anonymously Biased) |
unused:25 zeros:31 unused:1 age:4 biased_lock:1(1) lock:2(01) |
可偏向但未绑定线程 |
|
偏向锁(Biased) |
thread:54 epoch:2 age:4 biased_lock:1(1) lock:2(01) |
已偏向某线程 |
|
轻量级锁 |
ptr_to_lock_record:62 lock:2(00) |
指向栈中 Lock Record |
|
重量级锁 |
ptr_to_monitor:62 lock:2(10) |
指向 ObjectMonitor |
|
GC 标记 |
… lock:2(11) |
GC 使用 |
关键标志位:
biased_lock:1 表明支持偏向
lock:末两位决定锁状态
01:无锁或偏向锁
00:轻量级锁
10:重量级锁
11:GC 标记
重大限制:一旦对象的 identity hash code 被计算(如调用 System.identityHashCode(obj)),JVM 会将 hashCode 写入 Mark Word,永久失去偏向能力(因空间被占用)。
三、偏向锁工作机制
3.1 偏向锁的生命周期

3.2 详细流程解析
1:对象初始化(Unlocked Normal)
- 对象刚创建,Mark Word 处于 无锁状态;
- biased_lock=0, lock=01;
- 此时若计算 hashCode,则永久锁定在此状态。
2:进入可偏向状态(Anonymously Biased)
- JVM 启动后 默认延迟 4 秒(-XX:BiasedLockingStartupDelay=4000)才激活偏向锁;
- 所有新创建对象(未计算 hashCode)的 Mark Word 被重置为:[0…0 (25) | 0…0 (31) | 0 | age:0 | biased_lock:1 | lock:01]
- 此状态称为 “匿名偏向” —— 可被任意线程偏向,但尚未绑定。
为何延迟?避免 JVM 启动阶段大量临时对象被偏向,随后又被多线程竞争,导致频繁撤销。
3:首次加锁(Acquire Bias)
线程 T1 执行 synchronized(obj):
- 检查 Mark Word:
- biased_lock == 1 ✅
- lock == 01 ✅
- thread == 0(匿名偏向)✅
- 构造新的 Mark Word 值:
- thread = T1.id(54 位线程 ID)
- epoch = 当前 epoch(用于批量撤销)
- 保留 age, biased_lock=1, lock=01
- 执行 CAS 替换 Mark Word;
- 成功 → 对象目前偏向 T1;
- 失败 → 可能有并发竞争,进入 撤销流程。
✅ 关键点:这是唯一一次原子操作。后续 T1 加锁无需任何 CAS!
4:同线程重复加锁(Zero-Cost Entry)
T1 再次执行 synchronized(obj):
- 读取 Mark Word;
- 比较 thread == T1.id;
- 相等 → 直接进入临界区,无任何同步指令!
性能优势:比无锁还快(因省去了 volatile 读的内存屏障)。
5:其他线程竞争(Bias Revocation)
线程 T2 尝试获取已被 T1 偏向的锁:
- 发现 thread ≠ T2.id;
- 触发 偏向锁撤销(Bias Revocation);
- JVM 请求 安全点(Safepoint),暂停所有 Java 线程;
- 检查 T1 的状态:
- 撤销偏向,升级为轻量级锁;
- T1 的 Lock Record 被初始化;
- T2 自旋尝试获取轻量级锁。
- 将锁重置为 匿名偏向 或直接 偏向 T2;
- T2 通过 CAS 获取锁。
- 情况 A:T1 已退出所有同步块
- 情况 B:T1 仍在某个同步块中
- 恢复线程执行。
⚠️ 代价:STW(Stop-The-World)!这是偏向锁最大的缺点。
6:批量撤销与重偏向(Bulk Revocation & Rebias)
为避免频繁单个撤销,JVM 引入 epoch 机制:
- 每个类维护一个 epoch 值;
- 每次发生偏向撤销时,递增该类的 epoch;
- 对象 Mark Word 中也存储 epoch;
- 当线程发现 对象.epoch ≠ 类.epoch,说明该类的偏向已被批量撤销;
- 此时可安全地重新偏向当前线程,无需 STW。
目的:减少 STW 次数,提升高竞争场景下的恢复效率。
四、偏向锁的配置与控制
4.1 JVM 参数
|
参数 |
默认值 |
说明 |
|
-XX:+UseBiasedLocking |
true(JDK 14-) |
启用偏向锁 |
|
-XX:BiasedLockingStartupDelay=4000 |
4000 ms |
JVM 启动后延迟激活偏向锁 |
|
-XX:+BiasedLockingStartupDelay=0 |
— |
立即启用(测试用) |
|
-XX:-UseBiasedLocking |
— |
强制禁用 (推荐 JDK 15+) |
4.2 JDK 版本演进
|
JDK 版本 |
偏向锁状态 |
说明 |
|
JDK 6–14 |
默认启用 |
主流使用期 |
|
JDK 15 |
默认禁用 |
JEP 374:Disable and Deprecate |
|
JDK 17 |
完全移除 |
代码从 HotSpot 中删除 |
移除缘由:
现代应用多为高并发,偏向锁收益低;
撤销开销(STW)在容器化/低延迟场景不可接受;
Project Loom(虚拟线程)使偏向锁难以维护。
五、适用场景与反模式
✅ 适合使用偏向锁的场景
- 单线程长期持有锁
- GUI 应用(Swing/AWT 组件)
- 单线程处理消息队列
- 对象生命周期内仅被一个线程访问
- ThreadLocal 对象
- 线程私有缓存
- 锁竞争极少发生
- 配置对象、只读数据结构
❌ 不适合的场景(应禁用)
- 高并发、多线程竞争同一锁
- Web 服务器的共享缓存
- 生产者-消费者队列
- 使用 java.util.concurrent 包
- ReentrantLock、ConcurrentHashMap 等不走 JVM 锁体系
- 显式调用 System.identityHashCode()
- 导致对象无法进入偏向状态
- JDK 15+ 环境
- 默认禁用,强行启用无意义
六、实验验证与工具
6.1 使用 JOL 查看对象头
// Maven: org.openjdk.jol:jol-core
public class BiasedLockTest {
static final Object obj = new Object();
public static void main(String[] args) throws Exception {
Thread.sleep(5000); // 等待偏向锁激活
// 无锁状态
System.out.println("1. 初始: " + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
// 偏向锁状态
System.out.println("2. 偏向后: " + ClassLayout.parseInstance(obj).toPrintable());
}
// 计算 hashCode
obj.hashCode();
System.out.println("3. hashCode 后: " + ClassLayout.parseInstance(obj).toPrintable());
}
}
输出解读(64 位 JVM):
- 初始:0x0000000000000005 → 末三位 101 = 匿名偏向
- 偏向后:0x00007f8b1c000005 → 高 54 位为线程 ID
- hashCode 后:0x00000007a0d00e05 → 高 31 位为 hashCode,无法再偏向
6.2 使用 JFR 监控锁事件
启动录制:
java -XX:StartFlightRecording=settings=profile,duration=30s,filename=lock.jfr BiasedLockTest
在 JDK Mission Control (JMC) 中查看:
- Java Monitor Blocked 事件
- Thread Contention 时间线
- 可观察到偏向锁撤销时的 STW 停顿
七、常见问题
Q1:偏向锁真的“零开销”吗?
- 加锁时:是的,仅一次比较(if (mark.thread == current));
- 但有隐含成本:
- 对象头占用更多缓存行(因存储线程 ID);
- 撤销时的 STW 开销可能远超收益;
- 在 NUMA 架构下,跨 CPU 访问线程 ID 可能变慢。
Q2:为什么轻量级锁不用“偏向”思想?
- 轻量级锁依赖 Lock Record(位于线程栈),天然绑定线程;
- 但每次加锁仍需 CAS 更新 Mark Word 为指向 Lock Record 的指针;
- 偏向锁更进一步:连指针都不存,直接存线程 ID。
Q3:synchronized 和 ReentrantLock 的锁优化有何不同?
|
特性 |
synchronized |
ReentrantLock |
|
偏向锁 |
✅ 支持 |
❌ 不支持 |
|
轻量级锁 |
✅ JVM 优化 |
❌ 基于 AQS |
|
锁消除/粗化 |
✅ JIT 优化 |
❌ 无 |
|
可中断/超时 |
❌ |
✅ |
|
公平锁 |
❌ |
✅ |
synchronized 在无竞争单线程场景性能更优;ReentrantLock 在复杂并发控制场景更灵活。
八、总结
|
维度 |
说明 |
|
目标 |
优化“无竞争单线程”场景 |
|
核心机制 |
将锁永久分配给第一个获取者 |
|
性能收益 |
后续加锁零成本(仅一次比较) |
|
主要代价 |
撤销需 STW,高并发下得不偿失 |
|
历史地位 |
JVM 锁优化的重大里程碑 |
|
现状 |
已退出主流(JDK 17+ 移除) |
偏向锁体现了 “针对常见 case 极致优化” 的工程思想。虽然不再适用于现代高并发应用,但其设计思路(如 epoch 批量撤销)仍影响着 JVM 的其他子系统。
不知道从什么时候开始,在每个东西上面都有一个日子,秋刀鱼会过期,肉酱也会过期,连保鲜纸都会过期。我开始怀疑,在这个世界上,还有什么东西是不会过期的?” –《重庆森林》
存在过或许就是有些事物出现的最大价值。