---
管程：并发编程的万能钥匙
---

##### 前言

管程，对应的英文是 Monitor，也称监视器。

所谓管程，指的是管理共享变量以及对共享变量的操作过程，让它们支持并发。翻译为 Java 领域的语言，就是管理类的成员变量和成员方法，让这个类是线程安全的。那么管程是如何管的呢？

##### MESA 模型

在管程的发展史上，先后出现过三种不同的管程模型，分别是：Hasen 模型、Hoare 模型和 MESA 模型。其中，现在广泛应用的是 MESA 模型，并且 Java 管程的实现参考的也是 MESA 模型。

在并发编程领域，有两大核心问题：一个是互斥，即同一时刻只允许一个线程访问共享资源；另一个是同步，即线程之间如何通信、协作。这两大问题，管程都是能够解决的。

管程解决互斥问题的思路很简单，就是将共享变量及其对共享变量的操作统一封装起来。在下图中，管程 X 把对共享变量的操作封装了起来，也就保证了只有一个线程能够进入管程。这也是和 OOP 思想高度契合的。

![管程_互斥](https://i.loli.net/2019/03/18/5c8eff280ea48.png)

那管程是如何解决线程间同步的问题呢？

下面给出一个管程模型示意图，它详细描述了 MESA 模型主要组成部分。

在管程模型里，共享变量和对共享变量的操作是被封装起来的，图中最外层的框就代表封装的意思。框的上面只有一个入口，并且在入口旁边还有一个入口等待队列。当多个线程同时试图进入管程内部时，只允许一个线程进入，其他线程则在入口等待队列中等待。

管程里还引入了条件变量的概念，而且每个条件变量都对应有一个等待队列。

![](https://i.loli.net/2019/03/18/5c8f02ca403c0.png)

条件变量和等待队列的作用就是解决线程同步的问题。

以上面入对出队的例子讲解：

假设有个线程 T1 执行出队操作，如果当前队列为空，此时线程 T1 就需要在 "队列不空" 这个条件变量的等待队列中等待，即执行 A.wait() 来实现。

如果又有另外一个线程 T2 执行入队操作，入队操作执行成功之后，"队列不空" 这个条件对于线程 T1 来说就已经满足了，此时线程 T2 要通知 T1，告诉它需要的条件已经满足了，即执行 A.notify() 来通知 A 等待队列中的一个线程。当线程 T1 得到通知后，会从等待队列里面出来，但是出来之后不是马上执行，而是重新进入到入口队列等待队列里面。至于 notifyAll 方法，则是通知等待队列中所有的线程。

##### wait() 的正确姿势

对于 MESA 模型来说，有一个编程范式，就是需要在一个 while 循环里面调用 wait()。这个是 MESA 管程特有的。

```java
while(条件不满足){
  wait();
}
```

Hasen 模型、Hoare 模型和 MESA 模型的一个核心区别就是当条件满足后，如何通知相关线程。管程要求同一时刻只允许一个线程执行，那当线程 T2 的操作使得线程 T1 等待的条件满足时，T1 和 T2 究竟谁可以执行呢？

1. Hasen 模型里面，要求 notify() 方法代码最后，这样 T2 通知完 T1 后，T2 就结束了，然后 T1 再执行，这样1同一时刻只有一个线程执行。
2. Hoare 模型里面，T2 通知完 T1 后，T2 阻塞，T1 马上执行；等 T1 执行完，在唤醒 T2，也能保证同一时刻只有一个线程执行。但是相比 Hasen 模型，T2 多了一次阻塞唤醒操作。
3. MESA 模型里面，T2 通知完 T1 后，T2 还是会接着执行，T1 并不立即执行，仅仅是从条件变量的等待队列进入入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后，T2 也没有多余的阻塞唤醒操作。但是也有个副作用，就是当 T1 再次执行的时候，可能曾经满足的条件，现在已经不满足了，所以需要以循环的方式检验条件变量。

##### notify() 何时可以使用

除非经过深思熟虑，否则尽量使用 noyifyAll()。那么什么时候使用 notify 呢？需要满足以下三个条件：

1. 所有等待线程拥有相同的等待条件
2. 所有等待线程被唤醒后，执行相同的操作
3. 只需要唤醒一个线程

##### 总结

Java 参考了 MESA 模型，语言内置的管程（synchronized）对 MESA 模型进行精简。MESA 模型中，条件变量可以有多个，Java 语言内置的管程里只有一个条件变量，具体如下：

![](https://i.loli.net/2019/03/18/5c8f0eba877b4.png)

Java 内置的管程方案（synchronized）使用简单，synchronized 关键字修饰的代码块，在编译期会自动生成相关加锁和解锁的代码，但是仅支持一个条件变量；而 Java SDK 并发包实现的管程支持多个条件变量，不过并发包里的锁，需要开发人员自己进行加锁和解锁操作。

##### 摘自：

[管程：并发编程的万能钥匙](https://time.geekbang.org/column/article/86089)