Java并发编程学习——管程

什么是管程

管程:管理共享变量和操作共享变量的过程。为了解决信号量配对的复杂性以及分散在程序中降低了程序的可读性,管程通过一种数据结构对共享变量和对共享变量的操作进行了封装。

管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程更容易使用,所以 Java 选择了管程。

在Java1.5之前提供的是synchronized,wait,notify和notifyAll,这是java管程实现的一部分。

MESA模型

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

MESA模型实现互斥

互斥

这里的共享变量被封装起来了,其中对于共享变量queue的操作只能通过enq和deq,而这两个方法保证互斥性,只允许一个线程进入管程。

MESA墨香实现同步

同步

对于同步则通过增加条件变量和相应的等待队列

如果当条件变量不满足的时候则进入相应的等待队列,因为某个操作而导致条件允许的时候则唤醒相应的等待队列中的线程,并且再次获取锁来执行。因为要再次获取锁,所以这个条件只能是曾经满足过,所以当阻塞线程被唤醒再次获取锁需要执行的时候,有可能这个时候的条件又不允许了。

代码实现

对于入队操作,如果队列已满,就需要等待直到队列不满,所以这里用了notFull.await();。

对于出队操作,如果队列为空,就需要等待直到队列不空,所以就用了notEmpty.await();。

如果入队成功,那么队列就不空了,就需要通知条件变量:队列不空notEmpty对应的等待队列。

如果出队成功,那就队列就不满了,就需要通知条件变量:队列不满notFull对应的等待队列。

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和signal就类似于wait和notify。

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

1
2
3
while(条件不满足) {
wait();
}

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

  1. Hasen 模型里面,要求 notify() 放在代码的最后,这样 T2 通知完 T1 后,T2 就结束了,然后 T1 再执行,这样就能保证同一时刻只有一个线程执行。

  2. Hoare 模型里面,T2 通知完 T1 后,T2 阻塞,T1 马上执行;等 T1 执行完,再唤醒 T2,也能保证同一时刻只有一个线程执行。但是相比 Hasen 模型,T2 多了一次阻塞唤醒操作。

  3. MESA 管程里面,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。

notify() 何时可以使用

满足以下三个条件:

  1. 所有等待线程拥有相同的等待条件

  2. 所有等待线程被唤醒之后,执行相同的操作。

  3. 只需要唤醒一个线程。

-------------本文结束感谢阅读-------------