什么是管程
Java 在 1.5 之前仅提供了 synchronized
关键字以及 wait
、notify
、notifyAll
这三个方法,而没有提供信号量这样的编程原语。
因为 Java 采用的是 管程技术,synchronized
关键字以及 wati
、notify
、notifyAll
这三个方法都是管程
的组成部分,而管程
和信号量
是等价的,可以互相实现。但是管程更容易使用,所以 Java 选择了管程。
管程对应的英文是 Monitor
,也被翻译为 监视器,操作系统领域一般意义为管程。
所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。 翻译为 Java 领域的对应语言就是:管理类的成员变量
和成员方法
,让这个类是线程安全
的。
那么管程是如何实现的呢?
MESA 模型
管程在发展过程中出现过三种不同的模型:
Hasen
模型Hoare
模型MESA
模型
这里我想弄明白另外两种模型的区别,结果用中文搜索了一下,全他妈是把极客时间这篇文章当自己东西的,读书笔记就笔记,直接把别人东西当自己的真tmd不要脸,全是无效数据。
换英文搜了下,wiki真是牛逼,这里面把三种模型都介绍了。
现在广泛应用的是第三个 MESA
模型,并且 Java
中对于管程的实现参考的也是 MESA
模型,于是作者在这里重点介绍了这个模型。
并发领域有两个核心问题:
互斥
:同一时刻
只允许一个线程访问共享资源。同步
:线程之间如何通信
、协作
。
这两个问题管程都可以解决。先是 互斥
问题:
管程解决互斥的思路很简单:将共享变量以及对共享变量的操作统一封装起来。
在下图中,管程 X 将 共享变量 queue 这个队列和对队列的相关操作 入队 enq、出队 deq 都封装起来了。
线程A 和 线程B 如果想访问共享变量 queue,只能通过调用管程提供的 enq 和 deq 方法来实现,这两个方法保证了互斥性,只允许一个线程进入管程 。
管程模型与面向对象的设计思想高度契合,这可能也是 Java 选择管程的原因,互斥锁的用法背后就是管程。
同步
:
解决同步问题比较复杂,但是可以借鉴之前提到的就医流程,为了进一步理解,下面是一副 MESA 管程模型示意图,它详细描述了 MESA 模型的主要组成部分。
在管程模型里,共享变量
和对共享变量的操作是被封装
起来的,图中最外层框代表着封装
的意思。
框上只有一个入口
,入口旁边还有一个等待队列
,当多个线程同时视图进入管程内部时,只允许一个线程进入,其他线程在入口等待队列中等待。 这个过程就类似之前的分诊。
管程引入了条件变量的概念,每个条件变量都有对应的一个等待队列。 如图中的 变量A
和 变量B
分别有自己的等待队列
。
条件变量
和 等待队列
的作用就是为了解决 线程同步
问题,以下是一个例子:
假设有 线程T1
执行出队
操作,但是执行出队操作有个前提
,就是队列不能为空
的,队列不空这个前提条件就是管程里的 条件变量
。
如果 线程T1
进入管程
后恰好发现队列是空的,那么只能去条件变量对应的等待队列
中等待。
这个过程类似于大夫发现你需要验血,于是给你开了个验血的单子,你就去验血的队伍里排队。 线程T1
进入条件变量的 等待队列后,是允许其他线程进入管程的。
这就类似于你去验血的时候医生是可以给其他患者治病的。
假设另一个 线程T2
执行入队
操作,入队操作执行成功之后,队列不空
这个条件
对于 T1 已经满足
了,此时 线程T2
需要通知
T1
,告诉它需要的条件已经满足,当线程T1 得到通知后就会从 等待队列中出来,但是不是立即执行,而是重新进入 入口 的 等待队列中,这类似于做完检查需要重新分诊叫号,等待进入诊室。
条件变量以及等待队列的作用说清楚了,下面是 wait
、notify
、notifyAll
这三个操作的作用。
进入等待队列需要使用 wait
方法,前面说的 线程T1
发现队列不空这个条件不满足,则进入对应的等待队列,这个过程就是通过调用 wait
方法实现。、
同理当队列不空条件满足后,线程T2
需要调用 A.notify
来通知 A
等待队列中的一个线程,此时这个队列中只有 线程T1
,而 notifyAll
这个方法则会通知队列中的所有线程。
下面是另一段说明代码:实现了一个阻塞队列
,队列有两个操作分别是入队
和出队
,这两个操作都需要先获取锁
,类比管程模型中的入口
。
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();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
// 出队
void deq() {
lock.lock();
try {
while (队列已空) {
// 等待队列不空
notEmpty.await();
}
// 省略出队操作
// 出队后,通知可入队
notFull.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
- 对于入队操作,如果队列已满,则需要等待,直到队列不满,所以这里使用了
notFull.awit();
- 对于出队操作,如果队列为空,就需要等待直到队列不空,这里用了
notEmpty.awit();
- 如果入队成功,则队列不空,通知条件变量:队列不空
notEmpty
对应的等待队列 - 如果出队成功,则队列不满,通知条件变量:队列不空
notFull
对应的等待队列
在这段代码中,使用了 JDK 并发包
中的 Lock
和 Condition
,虽然看着陌生,但是其 awit
的语义 和 wait
一样,signal
和 notify
语义一样。
wait 的正确姿势
MESA
管程有一个编程范式:在 while
循环里调用 wait
,这是 MESA
管程特有的。
while(条件不满足) {
wait();
}
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
再次执行时,之前满足的条件可能已经不满足了,所以需要循环检验条件变量
。
notify 何时使用
之前曾经介绍过,除非经过深思熟练,否则尽量使用 notifyAll
,如果要使用 notify
,需要满足以下三个条件
:
- 所有等待线程拥有相同的等待条件
- 所有等待线程被唤醒后,执行相同操作
- 只需要唤醒一个线程
上面阻塞队列例子中,对于队列不满这个条件变量,其阻塞队列中的线程都是在等待队列不满这个条件。反应在下面这三行代码:
while(队列已满) {
// 等待队列不满
notFull.await();
}
对于所有等待线程
来说,都是执行这三行代码。重点是 while
里的等待条件完全相同。
而线程被唤醒后执行的操作也相同:
// 省略入队操作
// 入队后通知可出队
notEmpty.signal();
也满足了第三个使用 notify
的条件:只需要唤醒一个线程。所以上面例子中使用的是与 notify
相同语义的 signal 方法。
总结:
Java
参考 MESA
模型,语言内置的管程(synchronized
)对 MESA
模型进行了精简。
MESA
模型中,条件变量可以有多个,Java
语言内置的管程
中条件变量只有一个
。如下图所示:
Java
内置的管程方案使用简单, synchronized
关键字修饰代码块
,在编译期
会自动生成成对的加锁和解锁代码,但是仅支持一个条件变量
。
而 JDK 并发包
中实现的管程
支持多个条件变量
,但是并发包中的锁需要开发人员自己手动加锁
和解锁
。
并发编程中的两大核心问题:互斥和同步都可以由管程解决,并且很多工具类底层都是管程实现的,所以学好管程很关键。
个人总结:
这篇文章既拓展了知识面,让我明白了 Java
中 synchronized
背后的实现逻辑,也顺便知道了不同的管程模型。
同时图文并茂,又举了俩例子,让人更加容易理解管程是什么。
比如我之前就对 wait
和 notify
的使用总是不太理解,看完 7,8两篇文章,写了这些对应的代码,可以说基本掌握了。
Q.E.D.
Comments | 0 条评论