重点是 MESA 模型。
一
管程(Monitor),指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。也即管理类的成员变量和成员方法,让这个类是线程安全的。
在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。
这两大问题,管程都能解决。
二
管程将共享变量及其对共享变量的操作统一封装起来,以解决互斥问题;使用条件变量和条件变量等待队列解决线程同步问题。
Java 管程的实现参考了 MESA 管程模型(详见MESA管程模型):
- 在管程模型里,共享变量和对共享变量的操作是被封装起来的。
- 当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。
- 管程里每个条件变量都对应有一个等待队列。
比如实现一个线程安全的阻塞队列,可以将线程不安全的队列封装起来,对外提供线程安全的操作方法,例如入队操作和出队操作。
那怎么解决同步问题呢?
假设有个线程 T1 执行阻塞队列的出队操作,有个前提条件,就是阻塞队列不能是空的。
如果线程 T1 进入管程后恰好发现阻塞队列是空的,此时线程 T1 就去“队列不空”这个条件变量的等待队列中等待。
线程 T1 进入条件变量的等待队列后,是允许其他线程进入管程的。
再假设之后另外一个线程 T2 执行阻塞队列的入队操作,入队操作执行成功之后,“阻塞队列不空”这个条件对于线程 T1 来说已经满足了,此时线程 T2 要通知 T1,告诉它需要的条件已经满足了。
当线程 T1 得到通知后,会从等待队列里面出来,但是出来之后不是马上执行,而是重新进入到入口等待队列里面。
下面的代码用管程实现了一个线程安全的阻塞队列。阻塞队列有两个操作分别是入队和出队,这两个方法都是先获取互斥锁,类比管程模型中的入口。
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
//入队后,通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
//出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
三
MESA 管程有一个编程范式,就是需要在一个 while 循环里面调用 wait()。这个是 MESA 管程特有的。
while(条件不满足) {
wait();
}
MESA 管程里面,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。
四
管程是一个解决并发问题的模型,理解这个模型的重点在于理解条件变量及其等待队列的工作原理。
Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。
MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。
Java 内置的管程方案(synchronized)使用简单,synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。