Java 中的锁升级是 JVM 在 JDK 1.6 后对 synchronized
关键字的优化机制,根据线程竞争强度动态调整锁状态,从低开销的偏向锁逐步升级到高开销的重量级锁,以平衡性能与线程安全。下面结合原理、示例和对象头变化详细说明锁升级过程:
🔒 一、锁升级的四个阶段及触发条件
锁升级路径固定且不可逆:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
1. 无锁状态(No Lock)
- 场景:对象刚创建,未被任何线程加锁。
- 对象头 Mark Word:存储对象的 HashCode、分代年龄等,锁标志位
01
(最低两位)。 - 示例:
Object obj = new Object(); // 此时处于无锁状态
2. 偏向锁(Biased Lock)
- 触发条件:第一个线程访问同步块。
- 优化目标:消除无竞争时的同步开销(如单线程重复加锁)。
- 实现:
- 对象头记录偏向线程 ID 和偏向时间戳(epoch),锁标志位变为
101
。 - 同一线程再次进入同步块时,只需检查线程 ID 是否匹配,无需 CAS 操作。
- 对象头记录偏向线程 ID 和偏向时间戳(epoch),锁标志位变为
- 升级条件:第二个线程尝试获取锁(发生竞争)。
- 示例:
synchronized (lock) { // 线程 A 首次访问:启用偏向锁,记录线程 ID }
3. 轻量级锁(Lightweight Lock)
- 触发条件:偏向锁被撤销(有竞争发生)。
- 优化目标:避免线程阻塞,通过 CAS 自旋减少上下文切换。
- 实现:
- 线程在栈帧中创建 Lock Record,通过 CAS 将对象头替换为指向 Lock Record 的指针,锁标志位变为
00
。 - 若 CAS 失败,线程开始自适应自旋(JVM 根据历史成功率动态调整自旋次数)。
- 线程在栈帧中创建 Lock Record,通过 CAS 将对象头替换为指向 Lock Record 的指针,锁标志位变为
- 升级条件:自旋超过阈值(默认 10 次)或竞争线程数增加(如第三个线程加入)。
- 示例:
// 线程 B 尝试获取已被线程 A 偏向的锁 synchronized (lock) { // 偏向锁撤销,升级为轻量级锁,线程 B 通过自旋尝试 CAS }
4. 重量级锁(Heavyweight Lock)
- 触发条件:轻量级锁自旋失败。
- 实现:
- 对象头指向 Monitor 对象(操作系统互斥量 Mutex),锁标志位变为
10
。 - 未获取锁的线程进入阻塞队列,由操作系统调度(涉及用户态到内核态切换,开销大)。
- 对象头指向 Monitor 对象(操作系统互斥量 Mutex),锁标志位变为
- 特点:保证高竞争下的公平性,但线程阻塞和唤醒成本高。
- 示例:
// 多个线程(如 10 个)同时竞争锁 synchronized (lock) { // 轻量级锁自旋失败,升级为重量级锁,线程进入阻塞状态 }
⚙️ 二、完整锁升级示例及对象头变化
代码演示
public class LockUpgradeDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 阶段 1:偏向锁(线程 A 首次访问)
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread1: 获取偏向锁");
}
}).start();
Thread.sleep(1000); // 确保 Thread1 执行完
// 阶段 2:轻量级锁(线程 B 竞争)
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread2: 升级为轻量级锁");
}
}).start();
Thread.sleep(500);
// 阶段 3:重量级锁(多个线程高竞争)
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread" + i + ": 升级为重量级锁");
}
}).start();
}
}
}
对象头 Mark Word 变化(使用 JOL 工具观察)
// 添加依赖:org.openjdk.jol:jol-core
public static void main(String[] args) {
Object obj = new Object();
System.out.println("无锁状态:\n" + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println("偏向锁/轻量级锁:\n" + ClassLayout.parseInstance(obj).toPrintable());
}
// 高竞争下观察重量级锁
new Thread(() -> {
synchronized (obj) {
System.out.println("重量级锁:\n" + ClassLayout.parseInstance(obj).toPrintable());
}
}).start();
}
输出说明:
- 无锁:
Mark Word
末尾标志位01
- 偏向锁:
Mark Word
含线程 ID,标志位101
- 轻量级锁:
Mark Word
指向栈帧 Lock Record,标志位00
- 重量级锁:
Mark Word
指向 Monitor,标志位10
⚠️ 三、关键注意事项
-
锁升级不可逆:
重量级锁不会降级(即使竞争消失),因为降级开销大于直接释放锁。 -
偏向锁的延迟与禁用:
- 默认 JVM 启动后 4 秒才启用偏向锁(避免初始化竞争)。
- 可通过
-XX:BiasedLockingStartupDelay=0
立即启用,或-XX:-UseBiasedLocking
完全禁用(高并发场景推荐)。
-
优化建议:
- 减少临界区长度:避免长时间持锁(如 I/O 操作),降低竞争概率。
- 分段锁:如
ConcurrentHashMap
将大锁拆分为多个小锁。 - 避免过早膨胀:确保锁竞争强度与锁级别匹配(如避免在低竞争时误用重量级锁)。
💎 四、总结
- 核心价值:锁升级通过动态适配竞争强度(无竞争→低竞争→高竞争),在保证线程安全的同时最大化性能:
- 偏向锁:单线程重复访问(无竞争)
- 轻量级锁:短时间低竞争(CAS 自旋)
- 重量级锁:高竞争(阻塞队列保证公平性)
- 开发者角色:无需手动干预锁升级,但需合理设计同步块(控制粒度、时长)并监控竞争情况(如通过
jstack
分析锁状态)。 - 性能权衡:偏向锁撤销可能导致 STW,高竞争下重量级锁虽开销大,但可避免 CPU 空转。
通过理解锁升级机制,开发者能更好地定位并发瓶颈(如频繁锁升级导致的阻塞),并优化同步策略(如改用
ReentrantLock
细粒度控制)。