Java随笔-synchronized
synchronized 是 Java 最基础的同步机制,它的原理横跨 字节码 → JVM 运行时 → 操作系统内核 三个层面。
一、字节码层面:monitorenter 与 monitorexit
编译器对 synchronized 的处理有两种形式:
1. 同步代码块
public void syncBlock() {
synchronized (this) {
// 临界区
}
}
编译后字节码:
0: aload_0
1: dup
2: astore_1 // 将对象引用存入局部变量
3: monitorenter // 【进入】获取对象监视器锁
4: // 临界区代码...
11: aload_1
12: monitorexit // 【正常退出】释放锁
13: goto 21
16: astore_2 // 异常处理开始
17: aload_1
18: monitorexit // 【异常退出】释放锁(保证异常时也能释放)
19: aload_2
20: athrow
21: return
关键发现:即使代码中只有 1 个 synchronized 块,字节码里也会出现 两个 monitorexit —— 一个用于正常路径,一个用于异常路径。这是 JVM 保证锁一定会被释放的机制。
2. 同步方法
public synchronized void syncMethod() { }
编译后不会在方法体里插入 monitorenter/monitorexit,而是在方法的 访问标志(Access Flags) 中设置 ACC_SYNCHRONIZED(0x0020)。JVM 调用方法时会检查该标志,自动进行加锁/解锁。
二、对象头:锁状态的存储载体
synchronized 锁的所有状态信息都存储在对象的 对象头(Object Header) 中。
对象头结构(64 位 JVM,开启压缩指针)
┌─────────────────────────────────────────────────────────────┐
│ Object Header (128 bits) │
├─────────────────────────────┬───────────────────────────────┤
│ Mark Word (64 bits) │ Class Metadata Address (64) │
│ (存储锁状态、GC年龄、哈希码) │ (指向 Klass 对象的指针) │
└─────────────────────────────┴───────────────────────────────┘
Mark Word 的锁状态变化
| 锁状态 | 62 bits 内容 | biased_lock | lock |
|---|---|---|---|
| 无锁 | hash:31 + age:4 + ... |
0 | 01 |
| 偏向锁 | thread:54 + epoch:2 + age:4 |
1 | 101 |
| 轻量级锁 | ptr_to_lock_record:62 |
- | 00 |
| 重量级锁 | ptr_to_monitor:62 |
- | 10 |
| GC 标记 | - | - | 11 |
lock(2 bits)+ biased_lock(1 bit)共同决定当前对象的锁状态。
三、锁升级:synchronized 的核心优化
JDK 1.6 之前,synchronized 直接调用操作系统 mutex,性能极差。JDK 1.6 引入了 锁升级体系,根据竞争程度动态调整:
3.1 无锁 → 偏向锁
场景:只有一个线程反复进入同步块。
原理:
- 线程第一次获取锁时,用 CAS 将自己的 线程 ID 写入 Mark Word
- 之后该线程再次进入,只需检查 Mark Word 中的线程 ID 是否等于自己
- 相等则直接通过,无需任何原子操作,性能接近无锁
撤销:
- 当另一个线程尝试获取该锁时,JVM 需要在 安全点(STW) 撤销偏向锁
- 撤销成本较高,这也是 JDK 15 废弃偏向锁的原因
3.2 偏向锁 → 轻量级锁
场景:多线程交替执行(没有同时竞争)。
原理:
- 线程在自己的栈帧中创建 Lock Record(锁记录)
- 用 CAS 将对象头的 Mark Word 替换为指向该 Lock Record 的指针
- 成功:获得轻量级锁
- 失败:说明有其他线程在竞争,进入自旋
自旋:
- 线程在 CPU 上空转(循环检查锁是否释放),默认 10 次
- JDK 1.6 引入 自适应自旋:根据历史成功率动态调整自旋次数
- 自旋避免了线程阻塞/唤醒的内核态切换开销,但消耗 CPU
3.3 轻量级锁 → 重量级锁
场景:竞争激烈,自旋失败。
原理:锁膨胀(inflate) 为 ObjectMonitor。
对象头 Mark Word 原来指向 Lock Record
↓
修改为指向 ObjectMonitor 地址
↓
未获得锁的线程进入 _EntryList 阻塞(挂起)
膨胀过程:
- 检查 Mark Word 是否已指向 Monitor
- 如果没有,在堆中分配一个 ObjectMonitor 对象
- 用 CAS 将 Mark Word 改为指向 Monitor 的指针
- 竞争失败的线程调用 pthread_mutex_lock 进入内核态阻塞
四、重量级锁的底层:ObjectMonitor
当锁膨胀为重量级锁后,JVM 使用 ObjectMonitor(C++ 实现)管理线程排队和唤醒。
ObjectMonitor 核心字段
// hotspot/share/runtime/objectMonitor.hpp
class ObjectMonitor {
volatile markOop _header; // 保存原始 Mark Word
void* _object; // 指向锁对象
volatile int _count; // 重入次数
volatile int _waiters; // 调用 wait 的线程数
volatile int _recursions; // 锁重入次数
ObjectWaiter* _EntryList; // 【阻塞队列】竞争锁失败的线程
ObjectWaiter* _WaitSet; // 【等待队列】调用 wait() 的线程
Thread* volatile _owner; // 当前持有锁的线程
volatile int _cxq; // Contention List(竞争列表)
};
获取锁的流程(monitorenter)
线程尝试获取锁
│
▼
检查 _owner 是否为空
│
├── 为空 ──→ CAS 设置 _owner = 当前线程 ──→ 获取成功
│
└── 不为空 ──→ 检查 _owner 是否是当前线程 ──→ 是:_recursions++(重入)
│
└── 否:进入 _EntryList 阻塞(挂起)
释放锁的流程(monitorexit)
持有锁的线程执行完毕
│
▼
_recursions--(处理重入)
│
▼
_recursions == 0 ?
│
├── 否:继续执行
│
└── 是:释放锁
│
▼
从 _EntryList 或 _cxq 中唤醒一个线程
│
▼
被唤醒的线程重新竞争锁(非公平)
五、wait/notify 的底层实现
wait() 和 notify() 必须在 synchronized 块内调用,因为它们依赖 ObjectMonitor。
wait() 执行流程
void ObjectMonitor::wait(TRAPS) {
// 1. 检查当前线程是否持有锁
CHECK_OWNER();
// 2. 创建 ObjectWaiter 节点
ObjectWaiter node(THREAD);
// 3. 将当前线程加入 _WaitSet(等待队列)
AddWaiter(&node);
// 4. 释放锁(_owner = NULL, _recursions = 0)
exit(true, TRAPS);
// 5. 线程挂起(Park)
node._event->park();
// 6. 被 notify 唤醒后,重新竞争锁
enter(TRAPS);
}
关键点:
- wait() 会释放锁,然后线程进入 _WaitSet 挂起
- 被唤醒后,线程需要重新竞争锁(从 _WaitSet 移到 _EntryList 或直接竞争)
notify() 执行流程
void ObjectMonitor::notify(TRAPS) {
// 1. 检查当前线程是否持有锁
CHECK_OWNER();
// 2. 从 _WaitSet 中取出一个线程
ObjectWaiter* iterator = DequeueWaiter();
// 3. 将该线程移到 _EntryList(或 _cxq)
// 被唤醒的线程不会立即执行,需要等待当前线程释放锁后竞争
iterator->TState = ObjectWaiter::TS_ENTER;
iterator->notify_enter(this);
}
关键点:
- notify() 不会立即释放锁,只是将 _WaitSet 中的一个线程移到竞争队列
- 被唤醒的线程必须等当前线程执行完 synchronized 块释放锁后,才能参与竞争
六、锁消除与锁粗化(编译器优化)
除了运行时的锁升级,JVM 编译器还会做两项优化:
6.1 锁消除(Lock Elimination)
原理:逃逸分析发现锁对象不会被其他线程访问,直接去掉同步
public void method() {
StringBuffer sb = new StringBuffer(); // 局部变量,不会逃逸
sb.append("a"); // StringBuffer 的方法都是 synchronized
sb.append("b"); // 编译器会消除这些锁
}
6.2 锁粗化(Lock Coarsening)
原理:相邻的同步块使用同一个锁,合并为一个更大的同步块。
// 优化前
synchronized(lock) { doSomething(); }
synchronized(lock) { doSomethingElse(); }
// 优化后
synchronized(lock) {
doSomething();
doSomethingElse();
}
七、完整流程图
┌─────────────────────────────────────────────────────────────┐
│ synchronized 完整原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 源代码层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ synchronized(obj) { ... } / synchronized method │ │
│ └────────────────────┬────────────────────────────────┘ │
│ │ │
│ 字节码层 ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 同步块: monitorenter + monitorexit(×2) │ │
│ │ 同步方法: ACC_SYNCHRONIZED 标志位 │ │
│ └────────────────────┬────────────────────────────────┘ │
│ │ │
│ JVM 运行时层 ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 检查对象头 Mark Word │ │
│ │ │ │
│ │ 无锁 ──CAS──→ 偏向锁(记录线程ID) │ │
│ │ ↑ │ │
│ │ │ 线程ID匹配?是→ 直接通过 │ │
│ │ │ 否→ 撤销偏向锁 │ │
│ │ │ ↓ │ │
│ │ │ 轻量级锁(Lock Record + CAS) │ │
│ │ │ ↓ │ │
│ │ │ 自旋失败?是→ 锁膨胀 │ │
│ │ │ ↓ │ │
│ │ └──────────── 重量级锁(ObjectMonitor) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ _owner = 当前线程 │ │ │
│ │ │ 竞争失败→ _EntryList │ │ │
│ │ │ wait() → _WaitSet │ │ │
│ │ │ notify()→移入竞争队列│ │ │
│ │ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 操作系统层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 重量级锁调用 pthread_mutex_lock / pthread_cond_wait │ │
│ │ 线程阻塞 = 用户态→内核态切换(上下文切换) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
八、总结
| 层面 | 核心机制 |
|---|---|
| 字节码 | monitorenter/monitorexit(代码块)或 ACC_SYNCHRONIZED(方法) |
| 对象头 | Mark Word 存储锁状态(无锁/偏向/轻量/重量) |
| JVM 优化 | 锁升级(无锁→偏向→轻量→重量)、锁消除、锁粗化 |
| 重量级锁 | ObjectMonitor 管理 _owner、_EntryList、_WaitSet |
| 内核层 | pthread_mutex 实现线程阻塞与唤醒 |
synchronized 在 JDK 1.6 之后已经不再是"性能差"的代名词。JVM 通过锁升级体系,让大多数场景下它的性能与 ReentrantLock 相当,甚至在无竞争场景下更优(偏向锁零开销)。不过随着偏向锁在 JDK 15 被废弃,未来的趋势是 轻量级锁 + CAS 成为 synchronized 的主流路径。
更多推荐

所有评论(0)