Skip to content

管程(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. 总结:该选哪个?

特性synchronizedReentrantLock
实现层面JVM层面(C++ 实现)JDK层面(Java 实现)
易用性自动加锁解锁,代码简洁需手动 unlock,否则死锁
性能优化后性能极高(锁升级)竞争激烈时性能更稳定
功能性基础同步,单一等待队列支持公平锁、多条件、尝试获取锁

核心建议

如果你的需求简单,优先用 synchronized。如果你需要实现复杂的生产/消费逻辑,或者需要“公平性”,那么 ReentrantLock 是更好的选择。