Java 中 synchronized 的底层实现和锁的升降级机制是什么?
·
在 Java 中,synchronized 是解决多线程并发问题的关键机制,它的底层实现和锁的升降级机制涉及 JVM 和操作系统的协作。下面我们从底层原理到实际应用,逐步拆解这个复杂但重要的机制。
一、synchronized 的底层实现
1. 对象头与 Mark Word
每个 Java 对象在内存中分为三部分:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)。
synchronized 的锁信息存储在对象头的 Mark Word 中(32位/64位结构不同,以32位为例):
| 锁状态 | Mark Word(32位)内容 |
|---|---|
| 无锁 | 25位哈希码 + 4位分代年龄 + 1位偏向锁标志(0) + 2位锁标志(01) |
| 偏向锁 | 23位线程ID + 2位Epoch + 4位分代年龄 + 1位偏向锁标志(1) + 2位锁标志(01) |
| 轻量级锁 | 指向栈中锁记录的指针(30位) + 2位锁标志(00) |
| 重量级锁 | 指向操作系统互斥量(mutex)的指针(30位) + 2位锁标志(10) |
2. 锁的升级流程
synchronized 的锁状态会随着竞争情况逐步升级,目的是减少直接使用重量级锁的开销。流程如下:
(1)偏向锁(Biased Locking)
- 场景:适用于单线程重复进入同步块的场景(如循环内加锁)。
- 原理:Mark Word 中记录线程ID,后续该线程进入同步块时无需CAS操作。
- 优化意义:消除无竞争时的同步开销(连 CAS 都不需要)。
- 触发条件:默认启用(JVM 参数
-XX:+UseBiasedLocking)。
(2)轻量级锁(Thin Lock)
- 场景:低竞争环境(多线程交替执行,但未同时争抢锁)。
- 原理:
- 线程在栈帧中创建锁记录(Lock Record)。
- 通过 CAS 将 Mark Word 替换为指向锁记录的指针。
- 若成功,线程获得锁;若失败,说明有竞争,升级为重量级锁。
- 优化意义:避免直接调用操作系统互斥量,减少内核态切换。
(3)重量级锁(Heavyweight Lock)
- 场景:高竞争环境(多线程同时争抢锁)。
- 原理:依赖操作系统的 互斥量(mutex) 和 条件变量,未抢到锁的线程进入阻塞状态。
- 开销:涉及用户态到内核态的切换,性能最差。
3. 锁的降级
- 降级条件:在 HotSpot JVM 中,重量级锁不会降级到轻量级锁(设计选择,因降级带来的收益不如升级明显)。
- 特殊场景:在 G1 垃圾回收器 的某些阶段(如并发标记),会强制所有偏向锁撤销为无锁状态。
二、锁升级的底层细节
1. 偏向锁的获取与撤销
- 获取:
synchronized(obj) { // 第一次加锁时,Mark Word 记录线程ID // code... } - 撤销:当其他线程尝试获取锁时:
- 暂停持有偏向锁的线程(STW)。
- 检查原线程是否存活或仍在同步块中:
- 若已退出同步块,撤销为无锁状态。
- 若仍在同步块,升级为轻量级锁。
2. 轻量级锁的膨胀
当多个线程通过 CAS 竞争轻量级锁失败时:
- JVM 分配一个 重量级锁 所需的 monitor 对象(位于堆中)。
- Mark Word 更新为指向 monitor 的指针。
- 未抢到锁的线程进入 阻塞队列,等待唤醒。
3. monitor 对象的结构
每个 Java 对象关联一个 monitor(由 C++ 实现),结构如下:
- Owner:当前持有锁的线程。
- EntryList:阻塞等待锁的线程队列。
- WaitSet:调用
wait()后进入的线程队列。
三、为什么需要锁升级?
| 锁状态 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 无竞争时零开销 | 撤销时有 STW 开销 | 单线程重复访问 |
| 轻量级锁 | 避免内核态切换 | 自旋消耗 CPU | 低竞争、短时间同步 |
| 重量级锁 | 解决高竞争问题 | 性能最差(内核切换) | 高竞争、长时间同步 |
锁升级的核心目标:在无竞争或低竞争时,避免直接使用重量级锁的开销。
四、实战验证锁升级
1. 查看对象头工具
使用 JOL(Java Object Layout)工具打印对象头信息:
// 添加依赖:org.openjdk.jol:jol-core
public class LockUpgradeDemo {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
输出示例(片段):
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # 无锁状态(01)
...
加锁后:
0 4 (object header) 05 f0 00 00 # 偏向锁(01 + 偏向标志1)
2. 强制触发锁升级
public class HeavyLockDemo {
public static void main(String[] args) {
Object lock = new Object();
// 模拟高竞争
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (lock) {
try { Thread.sleep(100); }
catch (InterruptedException e) {}
}
}).start();
}
// 打印锁状态
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
输出将显示锁状态变为 重量级锁(10)。
五、常见误区与注意事项
- 锁升级不可逆:重量级锁不会降级(某些 JVM 可能例外)。
- 偏向锁延迟:JVM 默认在启动后 4 秒才启用偏向锁(
-XX:BiasedLockingStartupDelay=0可禁用延迟)。 - 锁消除优化:JIT 编译器可能直接去掉无竞争的锁(如局部变量的锁)。
- 锁粗化:连续对同一对象加锁可能合并为一个更大的锁。
总结
synchronized 的底层实现是 Java 并发优化的经典案例:
- 锁升级:通过偏向锁、轻量级锁、重量级锁的逐步升级,平衡性能与安全性。
- Mark Word:对象头中的元数据动态存储锁状态。
- 适用场景:低竞争时偏向锁和轻量级锁大幅提升性能,高竞争时重量级锁保证正确性。
理解这些机制,能帮助你更好地编写高效、线程安全的代码。
更多推荐


所有评论(0)