synchronized 关键字
基于 JDK 1.8+ 的 synchronized 关键字深度解析。
一、基础部分
1.1 synchronized 概述
synchronized 是什么?
Java 内置的关键字,用于实现线程同步,本质是对象锁(Monitor Lock)。
提供的三大特性:
| 特性 | 说明 |
|---|---|
| 原子性 | 同步块内的操作要么全部执行,要么全部不执行 |
| 可见性 | 释放锁时,将工作内存刷回主内存;获取锁时,从主内存重新读取 |
| 有序性 | 同一时刻只有一个线程执行同步块,表现为串行 |
【重点】核心思想:锁的是对象,不是代码
synchronized (obj) { // 锁的是 obj 对象
// 代码块
}1.2 三种使用方式
1. 修饰实例方法(锁 this)
public synchronized void method() {
// 锁的是 this 对象
}
// 等价于
public void method() {
synchronized (this) {
// ...
}
}2. 修饰静态方法(锁 Class 对象)
public static synchronized void staticMethod() {
// 锁的是 当前类.class 对象
}
// 等价于
public static void staticMethod() {
synchronized (MyClass.class) {
// ...
}
}3. 修饰代码块(锁指定对象)
Object lock = new Object();
public void method() {
synchronized (lock) {
// 锁的是 lock 对象
}
}三种方式对比
| 方式 | 锁对象 | 作用范围 | 适用场景 |
|---|---|---|---|
| 实例方法 | this | 当前对象 | 保护实例变量 |
| 静态方法 | Class | 类的所有实例 | 保护静态变量 |
| 代码块 | 指定对象 | 灵活控制 | 细粒度控制 |
【重点】实例锁和类锁是两把不同的锁,不互斥
public class Demo {
public synchronized void instanceMethod() { } // 锁 this
public static synchronized void staticMethod() { } // 锁 Demo.class
}
// 线程 A 调用 instanceMethod,线程 B 调用 staticMethod
// 两者不互斥!因为锁的是不同的对象1.3 锁对象的理解
┌────────────────────────────────────────────────────────────────┐
│ 锁对象规则 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 规则1:同一对象的不同同步方法 → 互斥 │
│ │
│ obj.syncMethod1() ←────互斥────→ obj.syncMethod2() │
│ │
├────────────────────────────────────────────────────────────────┤
│ │
│ 规则2:不同对象的相同同步方法 → 不互斥 │
│ │
│ obj1.syncMethod() ←──不互斥──→ obj2.syncMethod() │
│ │
├────────────────────────────────────────────────────────────────┤
│ │
│ 规则3:同步方法与非同步方法 → 不互斥 │
│ │
│ obj.syncMethod() ←──不互斥──→ obj.normalMethod() │
│ │
└────────────────────────────────────────────────────────────────┘二、对象内存布局
2.1 对象内存结构
┌─────────────────────────────────────────────┐
│ 对象内存布局 │
├─────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 对象头 (Object Header) │ │
│ │ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Mark Word (8 bytes) │ │ │ ← 存储锁信息
│ │ │ (哈希码、GC年龄、锁标志位) │ │ │
│ │ └─────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Class Pointer (4/8 bytes) │ │ │ ← 指向类元数据
│ │ └─────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Array Length (4 bytes) │ │ │ ← 仅数组对象有
│ │ └─────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 实例数据 (Instance Data) │ │ ← 成员变量
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 对齐填充 (Padding) │ │ ← 对齐到 8 字节
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘2.2 Mark Word 详细结构(64 位 JVM)
【重点】不同锁状态下 Mark Word 的结构变化
┌─────────────────────────────────────────────────────────────────────────┐
│ Mark Word (64 bits) │
├──────────────────────────────────────────────────────────────┬──────────┤
│ 内容 │ 锁标志位 │
├──────────────────────────────────────────────────────────────┼──────────┤
│ unused:25 | hashcode:31 | unused:1 | age:4 | biased:1 │ 01 │ 无锁
├──────────────────────────────────────────────────────────────┼──────────┤
│ threadID:54 | epoch:2 | unused:1 | age:4 | biased:1 │ 01 │ 偏向锁
├──────────────────────────────────────────────────────────────┼──────────┤
│ ptr_to_lock_record:62 │ 00 │ 轻量级锁
├──────────────────────────────────────────────────────────────┼──────────┤
│ ptr_to_monitor:62 │ 10 │ 重量级锁
├──────────────────────────────────────────────────────────────┼──────────┤
│ 空 │ 11 │ GC 标记
└──────────────────────────────────────────────────────────────┴──────────┘锁标志位含义:
| 标志位 | 锁状态 |
|---|---|
| 01 + biased=0 | 无锁 |
| 01 + biased=1 | 偏向锁 |
| 00 | 轻量级锁 |
| 10 | 重量级锁 |
| 11 | GC 标记 |
三、字节码层面
3.1 同步代码块
public void syncBlock() {
synchronized (this) {
// 代码
}
}编译后字节码:
0: aload_0
1: dup
2: astore_1
3: monitorenter ← 获取锁
4: ... ← 同步代码
7: aload_1
8: monitorexit ← 正常退出释放锁
9: goto 17
12: astore_2
13: aload_1
14: monitorexit ← 异常退出释放锁
15: aload_2
16: athrow
17: return【重点】为什么有两个 monitorexit?
- 第一个:正常执行完毕释放锁
- 第二个:发生异常时释放锁
保证无论正常还是异常,锁都能正确释放。
3.2 同步方法
public synchronized void syncMethod() {
// 代码
}方法访问标志:
flags: ACC_PUBLIC, ACC_SYNCHRONIZEDJVM 看到 ACC_SYNCHRONIZED 标志,会在方法调用前后自动获取/释放锁。
四、锁升级机制
4.1 锁升级概述
为什么需要锁升级?
JDK 1.6 之前,synchronized 直接使用重量级锁(OS Mutex),每次加锁都涉及内核态切换,性能很差。
升级路径:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁【重点】锁只能升级,不能降级
4.2 偏向锁详解
适用场景:只有一个线程访问同步块。
核心思想:锁"偏向"第一个获取它的线程。
获取过程
首次获取偏向锁:
│
▼
检查 Mark Word 是否可偏向(biased=1 且无 threadID)
│
├── 是 → CAS 将线程 ID 写入 Mark Word → 获取成功
│
└── 否 → 已偏向其他线程 → 撤销偏向
同一线程再次进入:
│
▼
比较 Mark Word 中的 threadID == 当前线程?
│
├── 是 → 直接进入(无任何同步操作)
│
└── 否 → 竞争,撤销偏向锁【重点】偏向锁的开销几乎为零(只是指针比较)。
偏向锁撤销
其他线程尝试获取锁
│
▼
等待全局安全点(STW)
│
▼
检查原持有者状态
│
├── 原线程已退出同步块 → 撤销偏向,设为无锁
│
└── 原线程仍在同步块中 → 升级为轻量级锁【注意】JDK 15+ 默认禁用偏向锁,JDK 18+ 彻底移除
4.3 轻量级锁详解
适用场景:多线程交替执行,竞争不激烈。
【重点】Lock Record 结构
┌─────────────────────────────────────────────────────────────────┐
│ 线程栈帧 │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Lock Record │ │
│ │ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Displaced Mark Word │ │ ← 备份原始 Mark Word │
│ │ └─────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ obj ptr (指向锁对象) │───┼───→ [锁对象] │
│ │ └─────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘获取过程
1. 在栈帧中创建 Lock Record
│
▼
2. 复制对象的 Mark Word 到 Lock Record(Displaced MW)
│
▼
3. CAS 尝试将对象头 Mark Word 替换为指向 Lock Record 的指针
│
├── 成功 → 获取轻量级锁
│
└── 失败 → 自旋重试 或 升级为重量级锁获取成功后的状态:
┌──────────────┐ ┌──────────────────┐
│ 锁对象 │ │ 线程栈帧 │
│ │ │ │
│ Mark Word: │─────────────→│ Lock Record: │
│ [ptr | 00] │ 指向 │ [Displaced MW] │
│ │ │ [obj ptr] ──────┼───→ 锁对象
└──────────────┘ └──────────────────┘【重点】指向关系:
- Mark Word 指向 Lock Record(指针)
- Lock Record.obj 指向锁对象(指针)
- Displaced MW 是原 Mark Word 的值拷贝,不是指针
重入处理
synchronized (obj) { // 第一次获取,创建 LR1,Displaced MW = 原值
synchronized (obj) { // 重入,创建 LR2,Displaced MW = null
// ...
} // 解锁 LR2,发现 Displaced MW = null,直接跳过
} // 解锁 LR1,CAS 恢复 Displaced MW自旋与自适应自旋
CAS 失败后,不立即阻塞,而是自旋等待:
for (int i = 0; i < spinCount; i++) {
if (CAS 成功)
return;
}
// 自旋失败,升级自适应自旋:JVM 根据历史自旋成功率动态调整自旋次数。
4.4 轻量级锁升级为重量级锁
【重点】升级条件(不是"有第三个线程就升级"!)
| 条件 | 说明 |
|---|---|
| 自旋次数超过阈值 | 默认约 10 次 |
| 自旋线程数过多 | 超过 CPU 核心数的一半 |
| 自旋过程中有新线程加入 | 竞争加剧 |
【核心】:升级条件是"竞争激烈程度",不是"线程数量"
【重点】升级过程(锁膨胀 Inflation)
1. 分配 ObjectMonitor 对象
│
▼
2. 从 Lock Record 复制原始 MW 到 Monitor._header
│
▼
3. 设置 Monitor._owner = 原持有线程
│
▼
4. CAS 修改对象头:ptr_to_LR|00 → ptr_to_Monitor|10
│
▼
5. 竞争线程进入 _EntryList 并 park() 阻塞【重点】原持有者继续持有锁,只是锁"升级"了
4.5 重量级锁详解
适用场景:多线程激烈竞争。
【重点】ObjectMonitor 结构
┌─────────────────────────────────────────────────────────────────┐
│ ObjectMonitor │
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ _header : 原始 Mark Word 备份(值拷贝,非指针) ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ _object : 指向锁对象的指针 ───→ [锁对象] ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ _owner : 指向持有锁线程的指针 ───→ [Thread 对象] ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ _recursions : 重入次数(int) ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ _EntryList : 等待获取锁的线程队列 ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ _cxq : 竞争队列(新到达的线程先进这里) ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ _WaitSet : 调用 wait() 的线程队列 ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────┘【重点】指向关系
┌──────────────┐ ┌─────────────────────┐
│ 锁对象 │ │ ObjectMonitor │
│ │ │ │
│ Mark Word: │─────────────→│ _header: [原MW值] │
│ [ptr | 10] │ 指向 │ _object: ─────────┼───→ 锁对象
│ │ │ _owner: ─────────┼───→ Thread 对象
└──────────────┘ │ _recursions: N │
└─────────────────────┘五、可重入性
5.1 什么是可重入
同一线程可以重复获取同一把锁:
public synchronized void outer() {
inner(); // 可以直接进入
}
public synchronized void inner() {
// 已经持有锁
}5.2 实现原理
轻量级锁:
第一次获取:Lock Record.Displaced MW = 原 Mark Word
重入获取:新建 Lock Record,Displaced MW = null(重入标记)
解锁时:遇到 null 直接跳过,不做 CAS重量级锁:
// 获取锁
if (_owner == currentThread) {
_recursions++; // 重入计数 +1
return;
}
// 释放锁
_recursions--;
if (_recursions == 0) {
_owner = null; // 真正释放
唤醒 _EntryList;
}六、wait/notify 机制
6.1 基本使用
synchronized (obj) {
while (!condition) {
obj.wait(); // 释放锁,进入 _WaitSet
}
}
synchronized (obj) {
obj.notify(); // 从 _WaitSet 移到 _EntryList
}6.2 工作原理
wait():
│
├── 释放锁(_owner = null)
│
└── 进入 _WaitSet 等待
notify():
│
├── 从 _WaitSet 取出一个线程
│
└── 放入 _EntryList(不是立即获得锁!)
【重点】notify() 不释放锁,被唤醒的线程需要重新竞争6.3 为什么 wait 要用 while 不用 if
// 错误写法
synchronized (obj) {
if (!condition) {
obj.wait();
}
doSomething(); // 可能 condition 又变了!
}
// 正确写法
synchronized (obj) {
while (!condition) {
obj.wait();
}
doSomething();
}原因:
- 虚假唤醒(Spurious Wakeup)
- 多个线程竞争,醒来时条件可能已不满足
七、synchronized vs ReentrantLock
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置 | JDK 实现(AQS) |
| 锁获取 | 阻塞 | 阻塞/非阻塞/超时/可中断 |
| 公平性 | 非公平 | 可选择 |
| 锁释放 | 自动(出作用域) | 手动(finally unlock) |
| Condition | 1 个(wait/notify) | 多个 |
| 锁状态查询 | 不支持 | isLocked() 等 |
| 锁升级优化 | 有 | 无 |
| 可扩展性 | 不可扩展 | 可扩展(模板方法) |
八、ObjectMonitor vs AQS
8.1 设计思想的一致性
| 核心概念 | ObjectMonitor | AQS |
|---|---|---|
| 状态变量 | _recursions | state |
| 持有者 | _owner | exclusiveOwnerThread |
| 等待队列 | _EntryList + _cxq | CLH 队列 |
| 条件等待 | _WaitSet | Condition 队列 |
| 阻塞/唤醒 | park/unpark | LockSupport.park/unpark |
8.2 主要差异
| 维度 | ObjectMonitor | AQS |
|---|---|---|
| 实现层面 | C++(JVM 内部) | Java |
| 锁升级 | 有偏向锁、轻量级锁 | 无 |
| 灵活性 | 固定行为 | 模板方法,可扩展 |
| 模式 | 仅独占 | 独占 + 共享 |
九、最佳实践
9.1 锁对象选择
// 推荐:使用私有专用锁对象
private final Object lock = new Object();
public void method() {
synchronized (lock) { }
}
// 不推荐:锁 this 或 Class(范围太大,易被外部干扰)
public synchronized void method() { }9.2 减小锁粒度
// 不推荐:整个方法加锁
public synchronized void process() {
step1(); // 不需要同步
step2(); // 需要同步
step3(); // 不需要同步
}
// 推荐:只锁必要的部分
public void process() {
step1();
synchronized (lock) {
step2();
}
step3();
}9.3 避免死锁
// 固定加锁顺序
synchronized (lockA) {
synchronized (lockB) { }
}
// 使用 tryLock 超时机制(ReentrantLock)
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try { } finally { lock.unlock(); }
}9.4 避免常见错误
// 错误:wait 使用 if
if (!condition) wait();
// 正确:wait 使用 while
while (!condition) wait();
// 避免:在同步块中调用外部方法(可能死锁)
synchronized (lock) {
externalMethod(); // 危险!
}十、总结
| 知识点 | 要点 |
|---|---|
| 本质 | 对象锁,锁的是对象不是代码 |
| Mark Word | 存储锁状态,不同状态结构不同 |
| 字节码 | 代码块用 monitorenter/exit,方法用 ACC_SYNCHRONIZED |
| 锁升级 | 无锁→偏向锁→轻量级锁→重量级锁,只升不降 |
| 偏向锁 | 单线程场景,CAS 记录线程 ID |
| 轻量级锁 | 交替执行,Lock Record + CAS + 自旋 |
| 重量级锁 | 激烈竞争,ObjectMonitor + EntryList |
| 可重入 | 轻量级用 null 标记,重量级用 _recursions |
| wait/notify | 必须持有锁,wait 用 while 循环 |
十一、个人复述理解
一开始是无锁状态。第一个线程 A 到来时,通过 CAS 将对象头 Mark Word 中的线程 ID 设置为自己,这就是偏向锁。之后 A 再次进入同步块,只需比较线程 ID 是否一致即可,几乎零开销。
当线程 B 来竞争时,发现 Mark Word 中的线程 ID 不是自己,触发偏向锁撤销(需要等待安全点 STW)。如果 A 还在同步块中,则升级为轻量级锁,A 继续持有;如果 A 已退出,则撤销偏向变为无锁,B 可以重新偏向。由于偏向锁撤销的 STW 开销,JDK 15 默认禁用,JDK 18 彻底移除。
轻量级锁的获取过程:线程在栈帧中创建 Lock Record,将 Mark Word 复制到 Lock Record 的 Displaced Mark Word 中,然后 CAS 尝试将对象头指向 Lock Record,同时 Lock Record 的 obj 指针指向锁对象。释放时,CAS 将 Displaced Mark Word 写回对象头即可。
如果 CAS 竞争失败,线程会进行自适应自旋:上次自旋成功则增加自旋次数,上次失败则减少。如果自旋仍然失败或竞争激烈,则升级为重量级锁。
重量级锁基于 ObjectMonitor 实现。膨胀过程:分配 ObjectMonitor,将原始 Mark Word(从 Lock Record 中)复制到 Monitor 的 _header 字段,设置 _owner 为原持有线程,最后 CAS 修改对象头指向 ObjectMonitor。获取锁失败的线程会加入 _cxq 队列并 park 阻塞。释放锁时,通过对象头直接找到 ObjectMonitor,将 _owner 设为 NULL(_recursions 必须为 0),然后从队列中选择继承者唤醒。由于新线程可以直接 CAS 抢锁而无需排队,所以 synchronized 是非公平的。
