多线程理论基础(二)

一. 前言
并发编程最重要解决方案就是管程,管程并不是一种实际的方案,而是一种方法论,重点在于用什么样方案解决并发的问题,管程实际上就是指的是管理共享变量以及对共享变量的操作过程,让他们支持并发;
- MESA模型
- Hoare模型
- Hasen模型
1.1 并发根本问题
并发领域最主要的两大问题:一是互斥,二是同步,互斥主要保证线程之间操作的原子性,同一个时刻只允许一个线程访问共享资源,同步则是线程之间通信,协作;
首先要明确一下管程的概念:管程又称为监视器,它是描述并实现对共享变量的管理与操作,使其在多线程下能正确执行的一个管理策略。可以理解成临界区资源的管理策略。
1.2 MESA模型示意图
MESA模型是管程的一种实现策略,Java使用的就是该策略。
- enterQueue:
管程的入口队列
,当线程在申请进入管程中发现管程已被占用,那么就会进入该队列并阻塞。 - varQueue:
条件变量等待队列
,在线程执行过程中(已进入管程),条件变量不符合要求,线程被阻塞时会进入该队列。 - condition variables:条件变量,存在于管程中,一般由程序赋予意义,程序通过判断条件变量执行阻塞或唤醒操作。
- 阻塞和唤醒:wait()和await()就是阻塞操作。notify()和notifyAll()就是唤醒操作。
这样一想在JAVA
中的synchronized
和Lock
在管程模型中,前者只有一个条件变量,后者可以维护多个条件变量;
执行过程:
- 多个线程进入
入口等待队列enterQueue
,JVM会保证只有一个线程能进入管程内部,Synchronized中进入管程的线程随机。 - 进入管程后通过条件变量判断当前线程是否能执行操作,如果不能跳到step3,否则跳到step4。
- 条件变量调用
阻塞
方法,将当前线程放入varQueue,等待其他线程唤醒,跳回step1。 - 执行相应操作,执行完毕后调用notify/notifyAll等唤醒操作,唤醒对应varQueue中的一个或多个等待线程。
- 被唤醒的线程会从varQueue放入enterQueue中,再次执行step1。
被唤醒的线程不会立即执行,会被放入enterQueue,等待JVM下一次选择运行,而正在运行的线程会继续执行,直到程序执行完毕。
二.MESA模型
1.互斥问题
管程解决互斥问题的思路很简单,就是将共享变量及其对共享变量的操作统一封装起来。在下图中,管程 X 将共享变量 queue 这个队列和相关的操作入队 enq()、出队 deq() 都封装起来了;线程 A 和线程 B 如果想访问共享变量 queue,只能通过调用管程提供的 enq()、deq() 方法来实现;enq()、deq() 保证互斥性,只允许一个线程进入管程。
2.同步问题
在管程模型里,共享变量和对共享变量的操作是被封装起来的,图中最外层的框就代表封装的意思。框的上面只有一个入口,并且在入口旁边还有一个入口等待队列。当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。这个过程类似就医流程的分诊,只允许一个患者就诊,其他患者都在门口等待。
MESA 模型解决同步问题可以类比去医院就医。患者首先需要排队等待医生叫好,医生诊断被叫到号的患者。期间,患者如果需要进行其他辅助的检查,比如说排个 X 光,就需要去等待拍 X 光的医生叫好。患者拍完 X 光之后,再次回到上一个医生那里,等待医生再次诊断。
3.实际例子
下面的代码实现的是一个阻塞队列,阻塞队列有两个操作分别是入队和出队,这两个方法都是先获取互斥锁,类比管程模型中的入口。
- 对于入队操作,如果队列已满,就需要等待直到队列不满,所以这里用了
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();
}
}
}
三. Hasen 模型和Hoare 模型
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 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。
四. 总结
Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简.
Java 内置的管程方案(synchronized)使用简单,synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。
五.关于我
Hello,我是球小爷,热爱生活,求学七年,工作三载,而今已快入而立之年,如果您觉得对您有帮助那就一切都有价值,赠人玫瑰,手有余香❤️.最后把我最真挚的祝福送给您及其家人,愿众生一生喜悦,一世安康!