管程概念

定义:管程又称为监视器,它是描述并实现对共享变量的管理与操作,使其在多线程下能正确执行的一个管理策略。可以理解成临界区资源的管理策略。

注:Java采用的是管程技术,synchronized关键字及wait()、notify()、notifyAll()这三个方法都是管程的组成部分

Java采用的管程模型

管程模型有三种,Hasen模型、Hoare模型和MESA模型。

并发编程最重要解决方案就是管程,管程并不是一种实际的方案,而是一种方法论。管程实际上就是指的是管理共享变量以及对共享变量的操作过程,让他们支持并发; Java采用的是MESA模型。

管程执行过程

image-20241019160355120.png

  • enterQueue:管程的入口队列,当线程在申请进入管程中发现管程已被占用,那么就会进入该队列并阻塞。
  • varQueue:条件变量等待队列,在线程执行过程中(已进入管程),条件变量不符合要求,线程被阻塞时会进入该队列。
  • condition variables:条件变量,存在于管程中,一般由程序赋予意义,程序通过判断条件变量执行阻塞或唤醒操作。
  • 阻塞和唤醒:wait()和await()就是阻塞操作。notify()和notifyAll()就是唤醒操作。

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

执行过程如下:

  1. 多个线程进入入口等待队列enterQueue,JVM会保证只有一个线程能进入管程内部,Synchronized中进入管程的线程随机。
  2. 进入管程后通过条件变量判断当前线程是否能执行操作,如果不能跳到step3,否则跳到step4。
  3. 条件变量调用阻塞方法,将当前线程放入varQueue,等待其他线程唤醒,跳回step1。
  4. 执行相应操作,执行完毕后调用notify/notifyAll等唤醒操作,唤醒对应varQueue中的一个或多个等待线程。
  5. 被唤醒的线程会从varQueue放入enterQueue中,再次执行step1。
  6. 被唤醒的线程不会立即执行,会被放入enterQueue,等待JVM下一次选择运行,而正在运行的线程会继续执行,直到程序执行完毕。

实际例子

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

  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();
    }  
  }
}

备注:await()和前面我们提到的wait()语义是一样的;signal()和前面我们提到的notify()语义是一样的。

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 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。

备注:对于MESA管程来说,有一个编程范式,就是需要在一个while循环里面调用wait()。这个是MESA管程特有的。