synchronized 锁的是什么?对象头结构详解
你一定见过这样的代码:
synchronized (this) {
// 临界区代码
}但你有没有想过:这个 this 对象到底扮演了什么角色?线程 A 进入 synchronized 块时,线程 B 为什么会被挡在外面?
synchronized 锁的不是代码,是对象。
synchronized 的本质:锁对象
当线程 A 执行 synchronized(obj) 时,JVM 会在 obj 的对象头里「做标记」——就像给门上了一把锁。
这把锁有三种形态:
| 锁状态 | 线程竞争 | 性能开销 | 说明 |
|---|---|---|---|
| 无锁 | 无竞争 | 无 | 正常状态,对象未被加锁 |
| 偏向锁 | 单线程 | 最低 | 偏向于第一个访问的线程 |
| 轻量级锁 | 多线程交替访问 | 较低 | 线程自旋等待,不阻塞 |
| 重量级锁 | 多线程同时竞争 | 较高 | 线程阻塞,需要 OS 介入 |
对象头结构:Mark Word
在 HotSpot JVM 中,每个 Java 对象在堆内存中分为两部分:
┌─────────────────────────────────────────────────────────────┐
│ 对象头 (Header) │
├─────────────────────────────────────────────────────────────┤
│ Mark Word (32位/64位,存储对象自身运行时数据) │
│ + 对象元数据指针 (指向方法区类元数据) │
│ + 数组长度 (如果是数组) │
├─────────────────────────────────────────────────────────────┤
│ 实例数据 (Instance Data) │
│ + 父类字段 + 本类字段 │
├─────────────────────────────────────────────────────────────┤
│ 对齐填充 (Padding) │
└─────────────────────────────────────────────────────────────┘重点来了——Mark Word 是 synchronized 锁机制的核心。
32 位 JVM Mark Word 位布局(重点!)
在不同锁状态下,Mark Word 的位布局完全不同:
┌────────────────────────────────────────────────────────────────────────┐
│ 32位 JVM Mark Word 完整结构 │
├──────────────────────┬───────┬────────┬─────────┬──────────────────────┤
│ 锁状态 │ 25bit │ 4bit │ 1bit │ 2bit │
│ │ │ │ biased │ lock │
│ │ │ age │ lock │ (锁类型标记) │
├───────────────────────┼────────┼────────┼─────────┼──────────────────────┤
│ 无锁 (001) │ hashCode(25bit) │ age │ 0 │ 01 │
│ │ 对象哈希码(如果已计算) │ │ │ │
├───────────────────────┼────────┼────────┼─────────┼──────────────────────┤
│ 偏向锁 (101) │ thread(23bit) │ epoch│ age │ 1 │ 01 │
│ │ 偏向线程ID │ │ │ │ │
├───────────────────────┼────────┼────────┼─────────┼──────────────────────┤
│ 轻量级锁 (00) │ 指向栈中锁记录的指针 (30bit) │ 00 │
├───────────────────────┼─────────────────────────────────────┼──────────────┤
│ 重量级锁 (10) │ 指向重量级锁 monitor 的指针 (30bit) │ 10 │
├───────────────────────┼─────────────────────────────────────┼──────────────┤
│ GC 标记 (11) │ 空 │ 11 │
└───────────────────────┴─────────────────────────────────────┴──────────────┘每种状态的详细解释
1. 无锁状态(biased_lock=0, lock=01)
┌─────────────────────────────────────────────────────────────────┐
│ 无锁状态 Mark Word │
├──────────┬─────────────────────────┬───────┬─────────┬──────────┤
│ 25 bit │ │ 4 bit │ 1 bit │ 2 bit │
│ hashCode │ 未使用 │ age │ 0 │ 01 │
└──────────┴─────────────────────────┴───────┴─────────┴──────────┘
│
└─ 如果调用过 System.identityHashCode(obj),会记录在这里当对象未被加锁时,25 位可以存储对象的 hashCode(如果已计算)。
注意:对象计算过 hashCode 后就不能进入偏向锁了,因为 hashCode 会占用偏向锁的空间。
2. 偏向锁状态(biased_lock=1, lock=01)
┌─────────────────────────────────────────────────────────────────┐
│ 偏向锁 Mark Word │
├─────────────┬──────────┬──────────┬───────┬─────────┬──────────┤
│ 23 bit │ 2 bit │ 4 bit │ 1 bit │ 1 bit │ 2 bit │
│ thread │ epoch │ age │ 1 │ biased │ 01 │
│ (偏向线程ID) │ (偏向时间戳) │ │ │ lock │ │
└─────────────┴──────────┴──────────┴───────┴─────────┴──────────┘
│
└─ 记录第一次获取偏向锁的线程ID偏向锁把线程 ID 直接记录在 Mark Word 中,后续该线程进入同步块时,只需比对线程 ID,无需任何同步操作。
3. 轻量级锁状态(lock=00)
┌─────────────────────────────────────────────────────────────────┐
│ 轻量级锁 Mark Word │
├─────────────────────────────────────────────────────────────────┬──────────┤
│ 30 bit │ 2 bit │
│ 指向栈中锁记录(Lock Record)的指针 │ 00 │
└─────────────────────────────────────────────────────────────────┴──────────┘
│
└─ 指向线程栈帧中的锁记录,锁记录中存储 Mark Word 的副本4. 重量级锁状态(lock=10)
┌─────────────────────────────────────────────────────────────────┐
│ 重量级锁 Mark Word │
├─────────────────────────────────────────────────────────────────┬──────────┤
│ 30 bit │ 2 bit │
│ 指向重量级锁 Monitor 对象指针 │ 10 │
└─────────────────────────────────────────────────────────────────┴──────────┘
│
└─ Monitor 对象包含: Owner + WaitSet + EntryList为什么 Mark Word 能「变脸」?
关键在于:Mark Word 是一个固定 8 字节(64位 JVM)或 4 字节(32位 JVM)的空间。
这就像一张桌子:
- 没人坐时,放文件
- 一个人长期坐,放这个人的名字
- 多人抢着坐,放椅子的位置信息
同一块内存,在不同场景下存储不同含义——这叫「空间复用」。
三种加锁方式的对比
public class SyncDemo {
// 方式1:锁 this
public synchronized void method1() {
// 锁的是当前实例对象
}
// 方式2:锁指定对象
public void method2() {
synchronized (this) {
// 锁的是当前实例对象
}
}
// 方式3:锁另一个对象
public void method3() {
synchronized (lockObj) {
// 锁的是 lockObj 对象
}
}
// 方式4:锁 Class 对象
public static synchronized void method4() {
// 锁的是 SyncDemo.class 对象
}
}面试追问方向
对象 A 和对象 B 都用 synchronized 修饰,它们会互斥吗? 不会,因为锁的是不同的对象。
synchronized 锁的是 Class 对象和锁 this,有什么区别? 前者所有实例共享同一把锁,后者每个实例各自有一把锁。
为什么 hashCode 会影响偏向锁? 因为偏向锁状态的 Mark Word 需要存储 23 位线程 ID,没有空间再存 hashCode 了。
Mark Word 在 64 位 JVM 中和 32 位有什么区别? 64 位 JVM 使用 64 位 Mark Word,但为了节省空间,JVM 开启了指针压缩(-XX:+UseCompressedOops)后,实际也是 8 字节。
