Java并发8:管程


Java并发8:管程

管程

Java采用的是管程技术,synchronized关键字及wait()notify()notifyAll()都是管程的组成部分。

管程和信号量是等价的,即用管程能够实现信号量,也能用信号量实现管程。

但是管程更容易使用,所以 Java 选择了管程。

管程(Monitor),很多将其翻译成“监视器”,这是直译。操作系统领域一般都翻译成“管程”,这是意译。

管程,即管理共享变量以及对共享变量的操作过程,让它们支持并发。

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

管程模型

Hasen模型/Hoare模型/MESA模型,Java参考的是MESA模型。

重申一遍并发核心问:分工(合理分配线程执行任务),互斥(同一时刻只有一个线程访问资源),同步(线程通信,协作)。

管程解决互斥

将共享变量及其对共享变量的操作统一封装。

管程 X 将共享变量 queue 队列和相关操作入队 enq()、出队 deq() 都进行封装;

线程 A 和线程 B 如果想访问共享变量 queue,只能通过调用管程提供的 enq()、deq() 方法来实现;

enq()、deq() 保证互斥性,只允许一个线程进入管程。

管程模型和面向对象高度契合。互斥锁用法,模型就是管程。

管程模型的代码化语义

管程解决同步

共享变量和对共享变量的操作被封装。

每个条件变量对应一个等待队列,不满足条件进入对应条件变量等待队列(wait()),满足被唤醒进入入口等待队列(notify()&notifyAll())。

MESA管程模型

代码说明:

对于入队操作,如果队列已满,就需要等待直到队列不满,所以这里用了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();
    }  
  }
}

Java内置管程方案

MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。

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

Java中管程示意图

wait()范式

while(条件不满足) {
  wait();
}

需要在while中调用wait(),MESA管程特有。

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

notify()何时使用

  • 所有等待线程拥有相同的等待条件;
  • 所有等待线程被唤醒后,执行相同的操作;
  • 只需要唤醒一个线程。

尽量使用notifyAll()

思考

wait() 方法,在 Hasen 模型和 Hoare 模型里面,都是没有参数的,而在 MESA 模型里面,增加了超时参数,参数是否有必要吗?

Hasen 模型和 Hoare 模型都保证一定唤醒线程。

MESA 模型因为采用了while循环,如果不加超时参数,仅使用notify()唤醒,那么wait()处的线程可能一直不被唤醒,wait()超时后,会到等待队列里抢锁。

别人的总结

管程的组成锁和0或者多个条件变量,java用两种方式实现了管程①synchronized+wait、notify、notifyAll②lock+内部的condition,第一种只支持一个条件变量,即wait,调用wait时会将其加到等待队列中,被notify时,会随机通知一个线程加到获取锁的等待队列中,第二种相对第一种condition支持中断和增加了时间的等待,lock需要自己进行加锁解锁,更加灵活,两个都是可重入锁,但是lock支持公平和非公平锁,synchronized支持非公平锁。

——linqw


文章作者: Wendell
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wendell !
评论
  目录