[Java并发-6]“管程”-java管程初探

并发编程这个技术领域已经发展了半个世纪了。有没有一种核心技术能够很方便地解决咱们的并发问题呢?这个问题, 我会选择 Monitor(管程)技术。
Java 语言在 1.5 以前,提供的惟一的并发原语就是管程,并且 1.5 以后提供的 SDK 并发包,也是以管程技术为基础的。除此以外,C/C++、C# 等高级语言也都支持管程。编程

什么是管程

操做系统原理课程告诉咱们,用信号量能解决全部并发问题。可是为何 Java 在 1.5 以前仅仅提供了 synchronized 关键字及 wait()、notify()、notifyAll() 这三个看似从天而降的方法?固然这里由于 Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而且安全

管程和信号量是等价的,所谓等价指的是用管程可以实现信号量,也能用信号量实现管程。

可是管程更容易使用,因此 Java 选择了管程。数据结构

管程,对应的英文是 Monitor,不少 Java 领域的同窗都喜欢将其翻译成“监视器”,这是直译。操做系统领域通常都翻译成“管程”,这个是意译,在这里我更倾向于使用“管程”。并发

管程,指的是管理共享变量以及对共享变量的操做过程,让他们支持并发。

翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。那管程是怎么管的呢?工具

MESA 模型

在管程的发展史上,前后出现过三种不一样的管程模型,分别是:Hasen 模型、Hoare 模型和 MESA 模型。其中,如今普遍应用的是 MESA 模型,而且 Java 管程的实现参考的也是 MESA 模型。因此咱们重点介绍一下 MESA 模型。spa

在并发编程领域,有两大核心问题:
一个是互斥,即同一时刻只容许一个线程访问共享资源;
另外一个是同步,即线程之间如何通讯、协做。这两大问题,管程都是可以解决的。操作系统

咱们先来看看管程是如何解决互斥问题的。线程

管程解决互斥问题的思路很简单,就是将共享变量及其对共享变量的操做统一封装起来。在下图中,管程 X 将共享变量 queue 这个队列和相关的操做入队 enq()、出队 deq() 都封装起来了;线程 A 和线程 B 若是想访问共享变量 queue,只能经过调用管程提供的 enq()、deq() 方法来实现;enq()、deq() 保证互斥性,只容许一个线程进入管程。从中能够看出,管程模型和面向对象高度契合的。而我在前面章节介绍的互斥锁用法,其背后的模型其实就是它。翻译

图片描述
管程模型的代码化语义3d

那管程如何解决线程间的同步问题的。

这个就比较复杂了,咱们来看下 MESA 管程模型示意图,它详细描述了 MESA 模型的主要组成部分。

在管程模型里,共享变量和对共享变量的操做是被封装起来的,图中最外层的框就表明封装的意思。框的上面只有一个入口,而且在入口旁边还有一个入口等待队列。当多个线程同时试图进入管程内部时,只容许一个线程进入,其余线程则在入口等待队列中等待。

管程里还引入了条件变量的概念,并且每一个条件变量都对应有一个等待队列,以下图,条件变量 A 和条件变量 B 分别都有本身的等待队列。

图片描述
MESA 管程模型图

那条件变量和等待队列的做用是什么呢?其实就是解决线程同步问题。你也能够结合上面提到的入队出队例子加深一下理解。

其余关于管程的定义,加深咱们的理解

管程是定义了一个数据结构和能为并发所执行的一组操做,这组操做可以进行同步和改变管程中的数据。这至关于对临界资源的同步操做都集中进行管理,凡是要访问临界资源的进程或线程,都必须先经过管程,由管程的这套机制来实现多进程或线程对同一个临界资源的互斥访问和使用。管程的同步主要经过condition类型的变量(条件变量),条件变量可执行操做wait()和signal()。管程通常是由语言编译器进行封装,体现出OOP中的封装思想,也如老师所讲的,管程模型和面向对象高度契合的。

假设有个线程 T1 执行出队操做,不过须要注意的是执行出队操做,有个前提条件,就是队列不能是空的,而队列不空这个前提条件就是管程里的条件变量。 若是线程 T1 进入管程后刚好发现队列是空的,那怎么办呢?等待啊,去哪里等呢?就去条件变量对应的等待队列里面等。此时线程 T1 就去“队列不空”这个条件变量的等待队列中等待。线程 T1 进入条件变量的等待队列后,是容许其余线程进入管程的。

再假设以后另一个线程 T2 执行入队操做,入队操做执行成功以后,“队列不空”这个条件对于线程 T1 来讲已经知足了,此时线程 T2 要通知 T1,告诉它须要的条件已经知足了。当线程 T1 获得通知后,会从等待队列里面出来,可是出来以后不是立刻执行,而是从新进入到入口等待队列里面。

条件变量及其等待队列咱们讲清楚了,下面再说说 wait()、notify()、notifyAll() 这三个操做。前面提到线程 T1 发现“队列不空”这个条件不知足,须要进到对应的等待队列里等待。这个过程就是经过调用 wait() 来实现的。若是咱们用对象 A 表明“队列不空”这个条件,那么线程 T1 须要调用 A.wait()。同理当“队列不空”这个条件知足时,线程 T2 须要调用 A.notify() 来通知 A 等待队列中的一个线程,此时这个队列里面只有线程 T1。至于 notifyAll() 这个方法,它能够通知等待队列中的全部线程。

下面的代码实现的是一个阻塞队列,阻塞队列有两个操做分别是入队和出队,这两个方法都是先获取互斥锁,类比管程模型中的入口。

  1. 对于入队操做,若是队列已满,就须要等待直到队列不满,因此这里用了notFull.await();
  2. 对于出队操做,若是队列为空,就须要等待直到队列不空,因此就用了notEmpty.await();
  3. 若是入队成功,那么队列就不空了,就须要通知条件变量:队列不空notEmpty对应的等待队列。
  4. 若是出队成功,那就队列就不满了,就须要通知条件变量:队列不满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 并发包里面的 Lock 和 Condition,这个例子只是先让你明白条件变量及其等待队列是怎么回事。

注意这里只是举个例子,这里的行为只是跟 管程相似,但并非具体实现,这里拿来举个例子。

wait() 的正确姿式

可是有一点,须要再次提醒,对于 MESA 管程来讲,有一个编程范式,就是须要在一个 while 循环里面调用 wait()。这个是 MESA 管程特有的

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() 什么时候可使用

还有一个须要注意的地方,就是 notify() 和 notifyAll() 的使用,前面章节,我曾经介绍过,
除非通过深思熟虑,不然尽可能使用 notifyAll()。那何时可使用 notify() 呢?
须要知足如下三个条件:

  1. 全部等待线程拥有相同的等待条件;
  2. 全部等待线程被唤醒后,执行相同的操做;
  3. 只须要唤醒一个线程。

好比上面阻塞队列的例子中,对于“队列不满”这个条件变量,其阻塞队列里的线程都是在等待“队列不满”这个条件,反映在代码里就是下面这 3 行代码。对全部等待线程来讲,都是执行这 3 行代码,重点是 while 里面的等待条件是彻底相同的

while (队列已满){
  // 等待队列不满
  notFull.await();
}

全部等待线程被唤醒后执行的操做也是相同的,都是下面这几行:

// 省略入队操做...
// 入队后, 通知可出队
notEmpty.signal();

同时也知足第 3 条,只须要唤醒一个线程。因此上面阻塞队列的代码,使用 signal() 是能够的。

总结

Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量能够有多个,Java 语言内置的管程里只有一个条件变量。具体以下图所示。

图片描述

Java 内置的管程方案(synchronized)使用简单,synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,可是仅支持一个条件变量;而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,须要开发人员本身进行加锁和解锁操做。

并发编程里两大核心问题——互斥和同步,均可以由管程来帮你解决。学好管程,理论上全部的并发问题你均可以解决,而且不少并发工具类底层都是管程实现的,因此学好管程,就是至关于掌握了一把并发编程的万能钥匙。

相关文章
相关标签/搜索