深浅模式
管程(Monitor)
更新: 1/23/2026 字数: 0 字
在 Java 并发编程中,管程(Monitor) 是实现线程同步的核心机制。我们常用的 synchronized 关键字,本质上就是 Java 对管程技术的封装
1. 什么是管程(Monitor)?
从概念上讲,管程是一堆共享数据和一套操作这些数据的方法的组合。它保证在任何时刻,只有一个线程能进入管程执行方法。
在 Java 的 HotSpot 虚拟机中,管程是由 ObjectMonitor(C++实现)实现的。每一个 Java 对象在出生时,都会自带一个“隐形”的 Monitor 对象。
Monitor 的内部结构
你可以把 Monitor 想象成一个带有“候诊室”的诊室:
- EntryList(入口队列):想要获取锁但没抢到的线程,会在这里排队等候
- Owner(持有者):当前正在诊室内看病(持有锁)的线程
- WaitSet(等待集合):已经进入诊室但因为某些条件不满足(调用了 wait()),暂时出来挂号等待的线程
2. synchronized 的底层原理
当你写下 synchronized(obj) 时,底层发生了什么?
字节码层面
通过 javap -v 查看代码,你会发现:
- 代码块:使用 monitorenter 和 monitorexit 指令
- 方法:在方法标志(flags)中打上 ACC_SYNCHRONIZED 标记
运行过程
- 抢锁:线程执行到 monitorenter,尝试获取对象的 Monitor
- 成功:如果 Monitor 的 count 为 0,该线程成为 Owner,count 加 1
- 重入:如果当前线程已经是 Owner,再次进入时 count 继续加 1(这就是可重入锁)
- 失败:如果锁被别人占了,线程进入 EntryList 阻塞等待
3. 实际例子:多线程操作同一变量
假设我们有一个计数器,多个线程同时对其进行自增操作。
java
class Counter {
private int count = 0;
public void increment() {
// 使用当前实例作为锁
synchronized (this) {
count++;
System.out.println(Thread.currentThread().getName() + " 修改后值: " + count);
}
}
}详细步骤解析
第一步:加锁(Locking)
当线程 A 执行到 synchronized 时,它会查看 Counter 对象的 Mark Word(对象头里的一个区域)。如果锁没被占用,它就把自己的线程 ID 记上去。
第二步:排队(Waiting)
此时线程 B 也来了。它发现锁被线程 A 拿着,于是它无法进入代码块,被迫进入 EntryList 变成 BLOCKED 状态。
第三步:等待与唤醒(Wait & Notify)
有时候,线程进入锁之后发现“条件不满足”(比如队列空了,没法取数据)。
- 线程 A 调用
obj.wait() - 释放锁:线程 A 释放 Monitor 所有权,进入 WaitSet 休息
- 唤醒:当另一个线程 B 完成了任务并调用
obj.notify(),线程 A 会被叫醒,重新进入 EntryList 去抢锁
4. 总结
synchronized 的核心就是 Monitor 机制。它通过对象头里的“锁标志位”来记录状态,通过 EntryList 管理排队线程,通过 WaitSet 管理挂起线程。
5. Synchronized
在 Java 中,我们通常使用 synchronized 配合 wait() 和 notifyAll() 来实现这种逻辑。
1. 实现逻辑
我们可以想象一个面包店的柜台:柜台只能放一个面包。如果柜台是空的,顾客(消费者)必须等待;如果柜台有面包,厨师(生产者)必须等待。
java
public class Bakery {
private boolean hasBread = false; // 条件变量:是否有面包
// 顾客线程调用的方法
public synchronized void take() {
// 1. 判断条件:如果没有面包,就进入 WaitSet 等待
while (!hasBread) {
try {
System.out.println(Thread.currentThread().getName() + " 发现没面包,进入等待...");
this.wait(); // 释放锁,进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2. 被唤醒并重新抢到锁后,执行业务逻辑
System.out.println(Thread.currentThread().getName() + " 拿到了面包!");
hasBread = false;
// 3. 唤醒其他正在等待的线程(比如提醒厨师去做面包)
this.notifyAll();
}
// 厨师线程调用的方法
public synchronized void bake() {
while (hasBread) {
try {
this.wait(); // 如果有面包,厨师就等一会儿再做
} catch (InterruptedException e) {
e.printStackTrace();
}
}
hasBread = true;
System.out.println("厨师做了一个新鲜面包!");
this.notifyAll(); // 唤醒正在等待拿面包的顾客
}
}2. 详细执行过程(排队与唤醒逻辑)
我们可以通过管程(Monitor)的视角来看看这个过程中线程是如何流转的:
第一阶段:不满足条件,主动退位
顾客线程 A 执行 take(),成功获取锁(进入 Owner 区域)。
检查 hasBread 发现是 false(没面包)。
执行 this.wait():
- 关键点:线程 A 会释放当前的锁
- 线程 A 从 Owner 区域移动到 WaitSet(等待集合)。此时它既不消耗 CPU,也不会去抢锁
第二阶段:其他线程工作并发出信号
厨师线程 抢到了锁,执行 bake()。
将 hasBread 改为 true,代表条件已满足。
执行 this.notifyAll():
- 关键点:这会把 WaitSet 里的线程 A “拍醒”
- 线程 A 从 WaitSet 移动到 EntryList(入口队列),重新开始竞争锁
第三阶段:重新抢锁并验证
厨师退出方法并释放锁。
顾客线程 A 在 EntryList 中竞争成功,重新进入 Owner 区域。
- 注意:线程 A 会从上次 wait() 的地方继续向下执行,重新检查 while 循环(这是为了防止“虚假唤醒”)
- 发现 hasBread 已经是 true,拿走面包,大功告成
3. 为什么面试官总问:为什么用 while 而不是 if?
在上面的代码中,我写的是 while (!hasBread)。如果换成 if:
- 线程 A 被唤醒后,会直接向下执行拿面包的逻辑
- 但如果在 A 被唤醒到重新抢到锁的这段间隙,另一个线程 B 突然冲进来把面包抢走了
- 如果用 if,线程 A 抢到锁后不会再检查,就会去拿一个并不存在的面包,导致程序出错
使用 while 可以保证线程在醒来后再次确认条件是否真的满足。
4. 总结:synchronized 的三个法宝
- 锁(Monitor Lock):保证同一时间只有一个线程在操作代码
- WaitSet:让不满足条件的线程“歇菜”,腾出地方给别人
- Notify:条件准备好了,叫醒歇着的人起来干活
6. ReentrantLock
如果说 synchronized 是“自动挡”,那么 ReentrantLock 就是“手动挡”。它不再直接使用 Object 里的 wait 和 notify,而是使用一个专门的接口:Condition
1. 核心对比:为什么需要它?
synchronized 有一个痛点:所有的线程都在同一个“候诊室”(WaitSet)里等。
- 厨师叫醒(notifyAll)时,所有的顾客和厨师都会被叫醒
- 结果:顾客抢到了锁发现还没面包(可能被别的顾客抢先了),厨师抢到了锁发现面包还在,大家又回去睡。这叫无效竞争
ReentrantLock + Condition 可以实现“定向唤醒”:你可以为顾客准备一个候诊室,为厨师准备另一个。
2. 代码实现:精准打击
我们用 Condition 改造一下之前的面包店:
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AdvancedBakery {
private final Lock lock = new ReentrantLock();
// 两个不同的“候诊室”
private final Condition 顾客等候区 = lock.newCondition();
private final Condition 厨师等候区 = lock.newCondition();
private boolean hasBread = false;
public void take() {
lock.lock(); // 手动加锁
try {
while (!hasBread) {
System.out.println("顾客进入专属等候区...");
顾客等候区.await(); // 类似于 wait()
}
hasBread = false;
System.out.println("顾客拿走面包");
// 精准叫醒:只叫醒厨师,不干扰其他顾客
厨师等候区.signal(); // 类似于 notify()
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 手动释放锁,一定要放在 finally 里
}
}
public void bake() {
lock.lock();
try {
while (hasBread) {
厨师等候区.await();
}
hasBread = true;
System.out.println("厨师做好了面包");
// 精准叫醒:只叫醒顾客
顾客等候区.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}3. 这里的“管程”实现有什么不同?
在底层,ReentrantLock 是基于 AQS (AbstractQueuedSynchronizer) 实现的。
- 队列管理:它内部维护了一个双向链表(同步队列),没抢到锁的线程就在链表里排队
- 多条件支持:一个 Lock 可以对应多个 Condition。每个 Condition 对象内部都有自己的一个等待队列
- 灵活性:
- 可中断:
lockInterruptibly()允许线程在等锁时响应中断 - 超时机制:
tryLock(time, unit)拿不到锁就跑,不会死等 - 公平性:可以设置
new ReentrantLock(true)让先排队的人先拿到锁(synchronized 是非公平的,靠抢)
- 可中断:
4. 总结:该选哪个?
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM层面(C++ 实现) | JDK层面(Java 实现) |
| 易用性 | 自动加锁解锁,代码简洁 | 需手动 unlock,否则死锁 |
| 性能 | 优化后性能极高(锁升级) | 竞争激烈时性能更稳定 |
| 功能性 | 基础同步,单一等待队列 | 支持公平锁、多条件、尝试获取锁 |
核心建议
如果你的需求简单,优先用 synchronized。如果你需要实现复杂的生产/消费逻辑,或者需要“公平性”,那么 ReentrantLock 是更好的选择。
