概念
管程封装了同步操作,对进程隐蔽了同步细节,简化了同步功能的调用界面。用户编写并发程序如同编写顺序(串行)程序。
引入目的:
- 把分散在各进程中的临界区集中起来进行管理
- 防止进程有意或无意的违法同步操作
- 便于用高级语言来书写程序,也便于程序正确性验证
MESA 模型
MESA 模型示意图如下:
在管程模型里,共享变量和对共享变量的操作是被封装起来的,图中最外层的框就代表封装的意思。
框的上面只有一个入口,并且在入口旁边还有一个入口等待队列。
当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。这个过程类似就医流程的分诊,只允许一个患者就诊,其他患者都在门口等待。
管程里还引入了条件变量的概念,而且每个条件变量都对应有一个等待队列,如上图,条件变量 A 和条件变量 B 分别都有自己的等待队列。
编程范式
对于 MESA 管程来说,有一个编程范式,就是需要在一个 while 循环里面调用 wait()。这个是 MESA 管程特有的。
1 | while(条件不满足) { |
之所以需要放在 while 循环里,这是因为在MESA 管程里面,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。
另外,除非经过深思熟虑,否则尽量使用 notifyAll(),原因如下:
notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。从感觉上来讲,应该是 notify() 更好一些,因为即便通知所有线程,也只有一个线程能够进入临界区。但那所谓的感觉往往都蕴藏着风险,实际上使用 notify() 也很有风险,它的风险在于可能导致某些线程永远不会被通知到。
假设我们有资源 A、B、C、D,线程 1 申请到了 AB,线程 2 申请到了 CD,此时线程 3 申请 AB,会进入等待队列(AB 分配给线程 1,线程 3 要求的条件不满足),线程 4 申请 CD 也会进入等待队列。我们再假设之后线程 1 归还了资源 AB,如果使用 notify() 来通知等待队列中的线程,有可能被通知的是线程 4,但线程 4 申请的是 CD,所以此时线程 4 还是会继续等待,而真正该唤醒的线程 3 就再也没有机会被唤醒了。
那什么时候可以使用 notify() 呢?需要满足以下三个条件:
- 所有等待线程拥有相同的等待条件
- 所有等待线程被唤醒后,执行相同的操作
- 只需要唤醒一个线程