Java并发8:管程
管程
Java采用的是管程技术,synchronized
关键字及wait()
、notify()
、notifyAll()
都是管程的组成部分。
管程和信号量是等价的,即用管程能够实现信号量,也能用信号量实现管程。
但是管程更容易使用,所以 Java 选择了管程。
管程(Monitor),很多将其翻译成“监视器”,这是直译。操作系统领域一般都翻译成“管程”,这是意译。
管程,即管理共享变量以及对共享变量的操作过程,让它们支持并发。
翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。
管程模型
Hasen模型/Hoare模型/MESA模型,Java参考的是MESA模型。
重申一遍并发核心问:分工(合理分配线程执行任务),互斥(同一时刻只有一个线程访问资源),同步(线程通信,协作)。
管程解决互斥
将共享变量及其对共享变量的操作统一封装。
管程 X 将共享变量 queue 队列和相关操作入队 enq()、出队 deq() 都进行封装;
线程 A 和线程 B 如果想访问共享变量 queue,只能通过调用管程提供的 enq()、deq() 方法来实现;
enq()、deq() 保证互斥性,只允许一个线程进入管程。
管程模型和面向对象高度契合。互斥锁用法,模型就是管程。
管程解决同步
共享变量和对共享变量的操作被封装。
每个条件变量对应一个等待队列,不满足条件进入对应条件变量等待队列(wait()
),满足被唤醒进入入口等待队列(notify()
¬ifyAll()
)。
代码说明:
对于入队操作,如果队列已满,就需要等待直到队列不满,所以这里用了notFull.await();
。
对于出队操作,如果队列为空,就需要等待直到队列不空,所以就用了notEmpty.await();
。
如果入队成功,那么队列就不空了,就需要通知条件变量:队列不空notEmpty
对应的等待队列。
如果出队成功,那就队列就不满了,就需要通知条件变量:队列不满notFull
对应的等待队列。
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();
}
}
}
Java内置管程方案
MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。
Java 内置的管程方案(synchronized
)使用简单,synchronized
关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。
wait()范式
while(条件不满足) {
wait();
}
需要在while
中调用wait()
,MESA管程特有。
Hasen模型、Hoare模型和MESA模型的一个核心区别就是当条件满足后,如何通知相关线程。
Hasen模型:notify()
放在代码最后,这样 T2 通知完 T1 后,T2 结束;T1 再执行,这样保证同一时刻只有一个线程执行。
Hoare模型:T2 通知完 T1 后,T2 阻塞,T1 马上执行;等T1 执行完,再唤醒 T2,也能保证同一时刻只有一个线程执行。相比Hasen模型,T2 多了一次阻塞唤醒操作。
MESA模型:T2 通知完 T1 后,T2 接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是notify()
不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。
但有副作用:当 T1 再次执行的时候,可能曾经满足条件,现在不满足,所以需要以循环方式检验条件变量。
notify()何时使用
- 所有等待线程拥有相同的等待条件;
- 所有等待线程被唤醒后,执行相同的操作;
- 只需要唤醒一个线程。
尽量使用notifyAll()
。
思考
wait()
方法,在 Hasen 模型和 Hoare 模型里面,都是没有参数的,而在 MESA 模型里面,增加了超时参数,参数是否有必要吗?
Hasen 模型和 Hoare 模型都保证一定唤醒线程。
MESA 模型因为采用了while
循环,如果不加超时参数,仅使用notify()
唤醒,那么wait()
处的线程可能一直不被唤醒,wait()
超时后,会到等待队列里抢锁。
别人的总结
管程的组成锁和0或者多个条件变量,java用两种方式实现了管程①synchronized+wait、notify、notifyAll②lock+内部的condition,第一种只支持一个条件变量,即wait,调用wait时会将其加到等待队列中,被notify时,会随机通知一个线程加到获取锁的等待队列中,第二种相对第一种condition支持中断和增加了时间的等待,lock需要自己进行加锁解锁,更加灵活,两个都是可重入锁,但是lock支持公平和非公平锁,synchronized支持非公平锁。
——linqw