深入理解 Java synchronized:从 Monitor 到锁升级机制
深入理解 Java synchronized:从 Monitor 到锁升级机制
前言
在学习 Java 高并发时,很多人都会接触到 synchronized,但往往停留在:
synchronized(lock) {
// 临界区
}
或者:
public synchronized void method() {
}
知道它能保证线程安全,却不清楚 JVM 底层到底做了什么。
本文将从 Monitor、对象头、锁升级、WaitSet 等角度,系统梳理 synchronized 的实现原理。
一、synchronized 的本质
synchronized 本质上依赖 JVM 的 Monitor(监视器) 实现同步。
可以简单理解为:
Object
↓
Monitor
├── Owner
├── EntryList
└── WaitSet
其中:
- Owner:当前持有锁的线程
- EntryList:等待获取锁的线程
- WaitSet:调用 wait() 后等待被唤醒的线程
当线程进入:
synchronized(lock) {
}
实际上是在尝试获取对象关联的 Monitor。
二、同步代码块是如何实现的
代码:
synchronized(lock) {
count++;
}
编译后会生成:
monitorenter
count++
monitorexit
执行流程:
monitorenter
↓
获取Monitor
↓
执行代码
↓
monitorexit
↓
释放Monitor
因此同步代码块是通过 JVM 字节码指令直接操作 Monitor。
三、同步方法是如何实现的
代码:
public synchronized void add() {
count++;
}
反编译后不会看到:
monitorenter
monitorexit
而是:
ACC_SYNCHRONIZED
方法标记。
很多人误以为:
同步方法 ≠ Monitor
实际上这是错误的。
ACC_SYNCHRONIZED 的作用
JVM 在调用方法时发现:
ACC_SYNCHRONIZED
会自动执行:
获取Monitor
↓
执行方法
↓
释放Monitor
可以理解为:
if(method.isSynchronized()){
monitor_enter();
}
invoke();
if(method.isSynchronized()){
monitor_exit();
}
因此:
- 同步代码块:显式使用 monitorenter/monitorexit
- 同步方法:JVM隐式获取和释放 Monitor
底层最终都是 Monitor。
四、实例方法和静态方法锁的是什么
实例同步方法:
public synchronized void test() {
}
等价于:
public void test() {
synchronized(this) {
}
}
锁对象:
this
静态同步方法:
public static synchronized void test() {
}
等价于:
public static void test() {
synchronized(Test.class) {
}
}
锁对象:
Test.class
五、Monitor 的结构
一个 Monitor 可以抽象为:
Monitor
├── Owner
├── EntryList
└── WaitSet
Owner
表示当前持有锁的线程。
例如:
Owner = ThreadA
表示 A 正在执行同步代码。
EntryList
存放竞争锁失败的线程。
例如:
ThreadA 获得锁
ThreadB 请求锁
ThreadC 请求锁
ThreadD 请求锁
结果:
Monitor
├── Owner = A
├── EntryList
│ B
│ C
│ D
└── WaitSet
这些线程会等待 A 释放锁。
WaitSet
WaitSet 与 EntryList 完全不同。
它存放的是:
主动调用 wait() 的线程
例如:
synchronized(lock){
while(queue.isEmpty()){
lock.wait();
}
consume();
}
执行:
lock.wait();
时会发生两件事:
1. 释放锁
Owner = null
2. 进入 WaitSet
Monitor
├── Owner = null
├── EntryList
└── WaitSet
A
六、notify() 的作用
生产者线程:
synchronized(lock){
queue.add(item);
lock.notify();
}
执行:
notify()
不会让线程立刻运行。
而是:
WaitSet
↓
EntryList
即:
Monitor
├── Owner = Producer
├── EntryList
│ Consumer
└── WaitSet
然后等待当前线程释放锁。
Consumer 再次竞争锁成功后:
wait() 返回
继续执行
七、为什么 wait() 必须在 synchronized 中调用
例如:
lock.wait();
如果当前线程没有持有锁。
JVM 根本不知道:
应该释放哪个Monitor?
因此直接抛出:
IllegalMonitorStateException
八、对象头与 Mark Word
HotSpot 对象结构:
Object
├── Mark Word
├── Klass Pointer
└── Instance Data
其中 Mark Word 保存:
- 锁状态
- hashCode
- GC年龄
- 线程信息
等元数据。
锁升级的本质:
不断修改 Mark Word
中的内容。
九、锁升级机制
JDK8 时代:
无锁
↓
偏向锁
↓
轻量级锁
↓
重量级锁
1. 无锁状态
对象刚创建:
Object obj = new Object();
此时:
Mark Word
=
无锁
2. 偏向锁
第一次进入:
synchronized(obj)
线程A获得锁。
对象头记录:
ThreadId = A
以后 A 再进入:
发现还是自己
直接成功。
无需 CAS。
3. 轻量级锁
当线程B出现:
A 持有锁
B 也想获取
偏向锁被撤销。
升级:
偏向锁
↓
轻量级锁
轻量级锁依赖:
CAS
竞争。
失败不会立即阻塞。
而是:
自旋
不断尝试获取锁。
4. 自旋锁
线程B:
CAS失败
↓
继续CAS
↓
继续CAS
因为 JVM 认为:
A 很快会释放锁
直接阻塞反而更贵。
5. 重量级锁
如果竞争越来越激烈:
A
B
C
D
E
持续争抢同一把锁。
JVM 判断:
自旋成本过高
升级:
轻量级锁
↓
重量级锁
此时真正启用 Monitor。
线程进入:
BLOCKED
状态。
十、为什么偏向锁被移除了
JDK6 时期:
CAS 比较昂贵
偏向锁能够避免 CAS。
收益明显。
随着 CPU 和 JVM 优化:
CAS 越来越便宜
偏向锁的收益越来越小。
而偏向锁撤销却非常复杂:
发现竞争
↓
暂停线程
↓
撤销偏向
↓
升级轻量级锁
甚至可能触发:
Stop The World
现代应用:
- 线程池
- ForkJoinPool
- 虚拟线程
线程切换频繁。
偏向锁适用场景越来越少。
因此:
- JDK15 默认禁用偏向锁
- 后续版本彻底移除
现代锁模型更接近:
无锁
↓
轻量级锁
↓
重量级锁
十一、一张图理解整个流程
synchronized
│
▼
获取Monitor
│
┌──────────────┴──────────────┐
▼ ▼
无竞争 存在竞争
│ │
▼ ▼
轻量级锁 CAS失败自旋
│
▼
持续竞争
│
▼
重量级锁
│
▼
Monitor结构
┌─────────────────────┐
│ Owner │
└─────────────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
EntryList WaitSet
抢锁失败线程 wait()线程
总结
synchronized 的核心并不是关键字本身,而是 JVM 提供的 Monitor 机制。
理解它可以抓住以下几个关键点:
- synchronized 最终都依赖 Monitor。
- 同步代码块使用 monitorenter/monitorexit。
- 同步方法使用 ACC_SYNCHRONIZED 标记。
- EntryList 保存抢锁失败线程。
- WaitSet 保存主动 wait() 的线程。
- 锁升级本质是 Mark Word 状态变化。
- 现代 JDK 已经移除了偏向锁。
- 当前主流模型是轻量级锁 + 自旋 + 重量级锁。
掌握这些内容后,再学习 AQS、ReentrantLock、线程池和并发容器时,会轻松很多。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)