8管程

管程

解决并发问题的核心技术:管程
管程:管理共享变量以及线程对共享变量的操作过程,让他们支持并发
翻译为Java领域的语言:管理类的变量和方法,让类线程安全

Java在1.5之前,提供的唯一的并发原语就是管程(Java采用的是管程技术synchronized关键字及wait、notify、notifyAll三个方法都是管程的组成部分)

管程(Monitor)和信号量等价,等价指管程、实现信号量可相互实现
(操作系统原理课程告诉我,信号量能解决所有并发问题)

并发编程领域两大核心问题:

  • 互斥,同一时刻只允许一个线程访问共享资源
  • 同步,线程之间如何通信、协作

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

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

MESA 模型

解决互斥问题

将共享变量及其对共享变量的操作统一封装起来
只能通过调用管程提供的 enq()、deq()方法访问共享变量queue,enq()、deq()互斥,只允许一个线程进入管程

解决同步

条件变量和等待队列解决线程同步问题
多个线程同时试图进入管程时,只允许一个进入,其他则在入口等待队列等待
管程里的条件变量都对应有一个等待队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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();
}
}
}

await和wait语义一样,signal和notify语义一样
wait需要在一个while循环里面调用,这个是MESA管程特有的。

notify何时可以使用

满足以下三个条件:

  • 所有等待线程拥有相同的等待条件;在同一个条件下等待。
  • 所有等待线程被唤醒后,执行相同的操作;
  • 只需要唤醒一个线程。

总结

Java内置的管程方案是对MESA模型进行了精简。
synchronized关键字修饰的代码块,编译期自动生成加锁和解锁的代码,仅支持一个条件变量;而Java SDK并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。