彻底搞懂 synchronized:从对象头到 Monitor,一文打穿 Java 锁底层
本文适合人群:学过 synchronized 关键字,但每次面试被问到底层总是一脸懵的同学。
看完本文你将能用人话解释清楚:锁究竟"长"在哪里?为啥会"升级"?Monitor 是个啥"保安"?
一、是什么:synchronized 的本质
很多人以为 synchronized 是 Java 语言层面的一个"功能",但实际上,它只是一个入口。
真正干活的,是 JVM 底层对对象内存结构的精心设计。
锁到底"住"在哪里?
当你在 Java 里 new 出一个对象,比如:
java
复制
Object lock = new Object();
这个对象在堆内存里,其实由三部分拼成:
┌─────────────────────────────┐
│ 对象头 (Object Header) │ ← synchronized 的灵魂!
├─────────────────────────────┤
│ 实例数据 (Instance Data)│ ← 你定义的那些字段
├─────────────────────────────┤
│ 对齐填充 (Padding) │ ← 凑整到 8 字节的倍数(CPU 寻址用)
└─────────────────────────────┘
锁,就住在对象头里。 具体说,是对象头里那 64 bit(8字节)的区域 —— Mark Word(标记字段)。
一句话总结:
synchronized所谓的"加锁",本质上就是多个线程抢着去修改这个 Mark Word 里的数据。
二、怎么用:三种锁状态对应三段代码
在深入原理之前,先来三段代码感受一下锁在不同场景下的"脸色变化"。
场景 1:偏向锁 —— VIP 专属通道
java
复制
public class BiasedLockDemo {
private static final Object lock = new Object();
public static void main(String[] args) {
// 只有主线程自己在反复获取锁,压根没竞争
for (int i = 0; i < 5; i++) {
synchronized (lock) {
System.out.println("第 " + i + " 次:只有我一个人,VIP 通道开启");
}
}
}
}
// 运行输出:
第 0 次:只有我一个人,VIP 通道开启
第 1 次:只有我一个人,VIP 通道开启
第 2 次:只有我一个人,VIP 通道开启
第 3 次:只有我一个人,VIP 通道开启
第 4 次:只有我一个人,VIP 通道开启
场景 2:轻量级锁 —— 礼貌的交接班
java
复制
public class LightweightLockDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程 1 拿到锁,执行完毕,礼貌退出");
}
});
t1.start();
t1.join(); // 等 t1 完全结束,再让 t2 上
Thread t2 = new Thread(() -> {
synchronized (lock) {
// 此时偏向锁失效,升级为轻量级锁
System.out.println("线程 2 拿到锁,平稳交接,无人阻塞");
}
});
t2.start();
}
}
// 运行输出:
线程 1 拿到锁,执行完毕,礼貌退出
线程 2 拿到锁,平稳交接,无人阻塞
场景 3:重量级锁 —— 真刀真枪的惨烈厮杀
java
复制
public class HeavyweightLockDemo {
private static final Object lock = new Object();
public static void main(String[] args) {
// 10 个线程同时冲!
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(200); // 故意霸占锁,加剧竞争
System.out.println(Thread.currentThread().getName() + " 终于抢到了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "激烈竞争线程-" + i).start();
}
}
}
// 运行输出(顺序不固定):
激烈竞争线程-0 终于抢到了!
激烈竞争线程-3 终于抢到了!
激烈竞争线程-7 终于抢到了!
... (依次逐个输出,其余线程老老实实排队阻塞)
三、重点细节:Mark Word 的"七十二变"与锁升级全流程
这才是本文的重头戏。Mark Word 就像一块动态电子公告板,它最低两位是"锁标志位",会根据竞争状态不断改写自己。
我们用一个"奶茶店争位子"的故事来串联全程👇
第 1 阶段:无锁状态(标志位 01,偏向位 0)
刚开业的奶茶店,位子没人坐,桌上只贴着餐厅的标准介绍。
对象刚被 new 出来,Mark Word 里存的是 HashCode + 分代年龄(GC 用的),干干净净,没有任何锁信息。
第 2 阶段:偏向锁(标志位 01,偏向位 1)
常客小明(线程 1)第一次来,直接把自己的名字贴到桌上:"这桌我的,以后我来不用叫号"。
底层动作:
线程 1 首次进入 synchronized 块,JVM 把线程 1 的 Thread ID 直接"刻"进这个对象的 Mark Word 里。
下次线程 1 再来,扫一眼标签是自己的,直接免检入场,连 CAS 都不做,性能极高。
Mark Word (偏向锁状态):
[ Thread ID (54bit) | epoch (2bit) | 分代年龄 (4bit) | 偏向标记=1 | 锁标志位=01 ]
第 3 阶段:轻量级锁(标志位 00)
小红(线程 2)也来了,看到桌上贴着"小明",偏向锁被打破!
两人约定用"抢凳子"游戏决定谁坐 —— 谁先把凳子搬到自己座位旁边(CAS 成功),谁就算赢,输的人原地踏步等一会儿。
底层动作(请结合栈帧知识理解!):
- JVM 在当前抢锁线程的栈帧里开辟一块小空间 —— 锁记录(Lock Record),先把 Mark Word 的副本拷进去。
- 线程用 CAS(比较并交换) 操作,尝试把对象头里的 Mark Word 整个替换成一个指向自己栈帧 Lock Record 的指针。
- 谁 CAS 成功,谁就拿到轻量级锁;没成功的线程就执行 自旋(while 循环空转),等一会儿再抢。
Mark Word (轻量级锁状态):
[ 指向线程栈帧中 Lock Record 的指针 (62bit) | 锁标志位=00 ]
💡 关键点:这整个过程都在用户态完成,没有操作系统介入,没有线程切换,速度依然很快。
第 4 阶段:重量级锁(标志位 10)
小红原地踏步转了 10 圈(自旋次数耗尽),还有小李、小王、小赵……一堆人都来了,全程空转 CPU 实在太浪费了。
此时店长出手,呼叫 "保安室"(Monitor),强制让所有抢锁失败的人坐到等候区排队,释放 CPU 资源,有空位再叫号。
触发条件:自旋超过阈值(默认约 10 次,或并发抢锁线程过多)
底层动作:
Mark Word 被再次改写,这次变成了一个指向 Monitor 对象的指针:
Mark Word (重量级锁状态):
[ 指向 Monitor 对象的指针 (62bit) | 锁标志位=10 ]
四、终极 Boss:Monitor 到底是个什么"保安室"?
Monitor(监视器)是 JVM 底层用 C++ 实现的数据结构,叫做 ObjectMonitor。这才是 synchronized 性能差的根本原因 —— 它需要操作系统介入,触发内核态切换。
你可以把 Monitor 想象成一个极其严格的保安室,它有三个核心区域:
┌──────────────────────────────────────────┐
│ Monitor 保安室 │
│ │
│ 👑 _owner(VIP 包厢) │
│ → 当前持有锁的那个线程 │
│ │
│ 😤 _EntryList(阻塞排队区) │
│ → 来晚了、CAS 失败的线程 │
│ → 被操作系统强制剥夺 CPU,进入 │
│ BLOCKED 状态,老老实实等叫号 │
│ │
│ 😴 _WaitSet(等待休息区) │
│ → 调用了 lock.wait() 的线程 │
│ → 主动放弃锁,进入 WAITING 状态 │
│ → 等别人 notify() 唤醒,再回 │
│ EntryList 重新参与竞争 │
└──────────────────────────────────────────┘
三者与线程状态的对应关系
| Monitor 区域 | 线程状态 | 如何进入 | 如何离开 |
|---|---|---|---|
_owner |
RUNNABLE(运行中) | 抢锁成功 | 执行完毕释放锁 |
_EntryList |
BLOCKED(阻塞) | 抢锁失败,OS 强制挂起 | 锁释放后被唤醒,重新竞争 |
_WaitSet |
WAITING(等待) | 主动调用 lock.wait() |
其他线程调用 notify()/notifyAll() |
💡
BLOCKED和WAITING的本质区别是什么?
—BLOCKED是被动的,是操作系统强行踢出去的;
—WAITING是主动的,是线程自己调用wait()躺平的。
五、对比 / 总结:一张图看透锁升级全流程
三种锁性能对比
| 锁状态 | 适用场景 | 是否需要 OS 介入 | 性能 |
|---|---|---|---|
| 偏向锁 | 单线程重复进入 | ❌ 不需要 | ⚡⚡⚡ 极高 |
| 轻量级锁 | 多线程交替执行,无真实并发 | ❌ 不需要 | ⚡⚡ 较高 |
| 重量级锁 | 多线程同时激烈竞争 | ✅ 需要,内核态切换 | ⚡ 较低 |
最后送你一句话:
synchronized不是洪水猛兽,理解了它的升级机制,你才能在代码设计时做出真正明智的选择——什么时候用它,什么时候换ReentrantLock,心里自然有数。
更多推荐

所有评论(0)