管程:并发编程的基石
程序员文章站
2022-05-05 09:41:36
...
点击上方
菜鸟飞呀飞
或者扫描下方二维码,即可关注微信公众号。
本文主要参考了极客时间上王宝令老师的《Java并发编程实战》,里面大部分内容来自这门课程,本文中的几张图也是参考课程里面的图画的。个人觉得这门课程比较基础,相对而言比较简单,老师讲解得也十分清晰,适合并发编程的入门,有需要的朋友可以点击文末的二维码连接去了解。
1. 定义
- Monitor在英语中直译是监视器的意思,但是在操作系统中通常被翻译为管程,是用来实现并发的一种技术,它解决了并发编程中的两大核心问题:互斥与同步。所以管程的定义是:用来管理共享变量以及对共享变量操作的过程。
- 历史上出现过三种管程模型,MESA模型、Hasen模型、Hoare模型。而在Java中,管程的实现是根据MESA模型实现的。
2. MESA模型
管程实现了并发编程领域的两大核心问题:互斥与同步,那么MESA模型是如何来实现互斥与同步的呢?
2.1 互斥
- 管程通过对共享变量以及对操作共享变量方法的进行了封装,在同一时刻,只有一个线程进入到管程中,这样就保证了只有一个线程来操作共享变量,实现了互斥的功能。
- 如上图中,管程X对共享变量count,以及对操作共享变量count的两个方法递增:increment()和递减:decrement()进行了封装,要想访问共享变量count,只能通过increment()和decrement()。由于管程保证了同时只允许一个线程进入,所以保证了increment()和decrement()的互斥性。
2.2 同步
- 如下图的管程模型图中,方框代表管程对共享变量以及操作共享变量方法的封装,在入口处有一个等待队列,当有多个线程试图进入管程时,管程只允许一个线程进入,其他的线程进入到等待队列中。
- 在管程中引入了条件变量的概念,每个条件变量都对应一个等待队列。如图中的条件变量A和B分别对应一个等待队列。
- 条件变量和等待队列的作用就是为了实现线程之间的同步问题。可以结合下面的例子理解。
假设有如下场景,对于共享变量count,初始值为0,我们需要对它进行递增或者递减操作,但是在进行操作时,需要满足如下条件,进行递增时,count的值不能大于等于10,进行递减时,count的值需要大于0。 - 假设线程T1要对共享变量进行递减操作,由于此时count值为0,所以不能进行递减操作,这个时候就应该让线程T1进行等待。去哪儿等待呢?去条件变量对应的等待队列里面进行等待。所以此时需要调用
count>0
这个条件变量对象的wait()方法,这样线程T1就进入到count>0
这个条件变量的等待队列中等待。 - 再假设线程T2要对共享变量进行递增操作,由于此时count=0,满足
count值不能大于等于10
这个条件,所以T2执行递增操作,操作之后count变为1,那么此时对于条件变量count>0
对于线程T1来说已经成立了,所以这个时候T2需要通知T1条件满足了。那么如何通知呢?通过调用count>0
这个条件变量对象的notify()或者notifyAll()方法。通知完成后,T1线程会从条件变量的等待队列中出来,但是此时T1不会立马执行,而是需要重新进入到管程入口处的等待队列中。 - 可以结合如下代码理解。在代码中使用了java.util.concurrent包下的类Lock和Condition,await()方法和wait()方法作用是一样的,signalAll()和notifyAll()作用是一样的。
public class Count {
volatile int count = 0;
final Lock lock = new ReentrantLock();
// 小于10条件变量
final Condition lessThanTen = lock.newCondition();
// 大于0条件变量
final Condition moreThanZero = lock.newCondition();
void increment(){
lock.lock();
try{
while(!count小于10){
// 小于10这个条件变量不满足,调用await()方法进入到条件变量的等待队列中
lessThanTen.await();
}
// 执行递增操作
// 递增完成以后,通知大于0这个条件等待队列中的线程
moreThanZero.signalAll();
}finally {
lock.unlock();
}
}
void decrement(){
lock.lock();
try{
while(!count大于0){
// 大于0这个条件变量不满足,调用await()方法进入到条件变量的等待队列中
moreThanZero.await();
}
// 执行递减操作
// 执行递减操作以后,通知小于10这个条件等待队列中的线程
lessThanTen.signalAll();
}finally{
lock.unlock();
}
}
}
- 通过对上面MESA管程模型的介绍,到这里相信你对管程有了一定的认识。那管程在Java中到底是如何实现的呢?
3. synchronized
- Java语言中为我们提供了synchronized关键字来实现锁。synchronized关键词实现的锁是隐式锁,为什么称之为隐式锁呢?是因为在编译器编译Java文件时,当碰到synchronized时,会自动加上加锁指令和解锁指令,即monitorenter和monitorexit,这一步对于开发人员来说是透明的,所以说它是隐式锁。它的底层实现就是通过管程实现的,前面我们介绍的管程模型中,可以支持多个条件变量,但是synchronized的管程实现只支持一个条件变量。它的管程示意图如下:
- 那么在虚拟机中,是如何实现管程的呢?那就是ObjectMonitor这个对象了。在openJDK的源码中,有这样两个源文件:objectMonitor.cpp和objectMonitor.hpp。前者的文件中实现了锁的获取、释放等方法,后者定义了前者需要的头文件。在objectMonitor.hpp文件中定义了这样一个结构:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
// 条件等待队列
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
// 入口等待队列
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
- 在ObjectMonitor这个结构中,有两个重要的属性:_WaitSet和_EntryList,这两个属性分别对应管程中条件等待队列和入口等待队列。
4. Lock与Condition
Lock和Condition是java.util.concurrent包下的类,与synchronized关键字实现锁的原理不一样,Lock是在Java层面实现的,而synchronized是通过JVM虚拟机实现的锁。
- Lock和Condition也是通过管程模型来实现锁的。其中Lock是是用来实现互斥的,Condition是用来实现同步的。
- 与synchronized不同的是,Lock与Condition实现的管程,支持多条件变量,而synchronized的管程实现只支持单个条件变量。
5. 总结
- 本文主要介绍了管程的模型以及管程是如何来实现并发的,
- 本文结合Java中synchronized和Lock介绍了管程的实现。
本文的内容更偏于个人的学习笔记,很多地方讲解得不是很详细,如果想了解更加详细的,可以去看看极客时间上《Java并发编程实战》这门课程,下方是课程的二维码链接。
上一篇: 秋招准备-Java-并发编程-显式锁Lock(三)
下一篇: Lock中的Condition