Skip to content

synchronized 锁的是什么?对象头结构详解

你一定见过这样的代码:

java
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)的空间

这就像一张桌子:

  • 没人坐时,放文件
  • 一个人长期坐,放这个人的名字
  • 多人抢着坐,放椅子的位置信息

同一块内存,在不同场景下存储不同含义——这叫「空间复用」


三种加锁方式的对比

java
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 对象
    }
}

面试追问方向

  1. 对象 A 和对象 B 都用 synchronized 修饰,它们会互斥吗? 不会,因为锁的是不同的对象。

  2. synchronized 锁的是 Class 对象和锁 this,有什么区别? 前者所有实例共享同一把锁,后者每个实例各自有一把锁。

  3. 为什么 hashCode 会影响偏向锁? 因为偏向锁状态的 Mark Word 需要存储 23 位线程 ID,没有空间再存 hashCode 了。

  4. Mark Word 在 64 位 JVM 中和 32 位有什么区别? 64 位 JVM 使用 64 位 Mark Word,但为了节省空间,JVM 开启了指针压缩(-XX:+UseCompressedOops)后,实际也是 8 字节。

基于 VitePress 构建