【Java并发基础】使用“等待—通知”机制优化死锁中占用且等待解决方案

前言

在前篇介绍死锁的文章中,咱们破坏等待占用且等待条件时,用了一个死循环来获取两个帐本对象。html

// 一次性申请转出帐户和转入帐户,直到成功
while(!actr.apply(this, target))
  ;

咱们提到过,若是apply()操做耗时很是短,且并发冲突量也不大,这种方案仍是能够。不然的话,就可能要循环上万次才能够获取锁,这样的话就太消耗CPU了!java

因而咱们给出另外一个更好的解决方案,等待-通知机制
如果线程要求的条件不知足,则线程阻塞本身,进入等待状态;当线程要求的条件知足时,通知等待的线程从新执行。编程

Java是支持这种等待-通知机制的,下面咱们就来详细介绍这个机制,并用这个机制来优化咱们的转帐流程。
咱们先经过一个就医流程来了解一个完善的“等待-通知”机制。多线程

就医流程—完整的“等待—通知”机制

在医院就医的流程基本是以下这样:并发

  1. 患者先去挂号,而后到就诊门口分诊,等待叫号;
  2. 当叫到本身的号时,患者就能够找医生就诊;
  3. 就诊过程当中,医生可能会让患者去作检查,同时叫一位患者;
  4. 当患者作完检查后,拿着检查单从新分诊,等待叫号;
  5. 当医生再次叫到本身时,患者就再去找医生就诊。

咱们将上述过程对应到线程的运行状况:app

  1. 患者到就诊门口分诊,相似于线程要去获取互斥锁;
  2. 当患者被叫到号时,相似于线程获取到了锁;
  3. 医生让患者去作检查(缺少检查报告不能诊断病因),相似于线程要求的条件没有知足;
    患者去作检查,相似于线程进入了等待状态;而后医生叫下一个患者,意味着线程释放了持有的互斥锁;
  4. 患者作完检查,相似于线程要求的条件已经知足;患者拿着检查报告从新分诊,相似于线程须要从新获取互斥锁。

一个完整的“等待—通知”机制以下:
线程首先获取互斥锁,当线程要求条件不知足时,释放互斥锁,进入等待状态;当条件知足时,通知等待的线程,从新获取锁优化

必定要理解每个关键点,还须要注意,通知的时候虽然条件知足了,可是不表明该线程再次获取到锁时,条件仍是知足的。this

Java中“等待—通知”机制的实现

在Java中,等待—通知机制能够有多种实现,这里咱们讲解由synchronized配合wait()notify()或者notifyAll()的实现。spa

如何使线程等待,wait()

当线程进入获取锁进入同步代码块后,如果条件不知足,咱们便调用wait()方法使得当前线程被阻塞且释放锁线程

上图中的等待队列和互斥锁是一一对应的,每一个互斥锁都有本身的独立的等待队列(等待队列是同一个)。(这句话还在暗示咱们后面唤醒线程时,是唤醒对应锁上的线程。)

如何唤醒线程,notify()/notifyAll()

当条件知足时,咱们调用notify()或者notifyAll(),通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经知足过

咱们要在相应的锁上使用wait() 、notify()和notifyAll()。
须要注意,这三个方法能够被调用的前提是咱们已经获取到了相应的互斥锁。因此,咱们会发现wait() 、notify() notifyAll()都是在synchronized{...}内部中被调用的。若是在synchronized外部调用,JVM会抛出异常:java.lang.IllegalMonitorStateException。

使用“等待-通知”机制重写转帐

咱们如今使用“等待—通知”机制来优化上篇的一直循环获取锁的方案。首先咱们要清楚以下以下四点:

  1. 互斥锁:帐本管理员Allocator是单例,因此咱们可使用this做为互斥锁;
  2. 线程要求的条件:转出帐户和转入帐户都存在,没有被分配出去;
  3. 什么时候等待:线程要求的条件不知足则等待;
  4. 什么时候通知:当有线程归还帐户时就通知;

使用“等待—通知”机制时,咱们通常会套用一个“范式”,能够看做是前人的经验总结用法。

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

这个范式能够解决“条件曾将知足过”这个问题。由于当wait()返回时,条件已经发生变化,使用这种结构就能够检验条件是否还知足。

解决咱们的转帐问题:

class Allocator {
    private List<Object> als;
    // 一次性申请全部资源
    synchronized void apply(Object from, Object to){
        // 经典写法
        while(als.contains(from) || als.contains(to)){ 
            // from 或者 to帐户被其余线程拥有
            try{
                wait(); // 条件不知足时阻塞当前线程
            }catch(Exception e){
            }   
        }
        als.add(from);
        als.add(to);  
    }
    // 归还资源
    synchronized void free(
        Object from, Object to){
        als.remove(from);
        als.remove(to);
        notifyAll();   // 归还资源,唤醒其余全部线程
    }
}

一些须要注意的问题

sleep()和wait()的区别

sleep()wait()均可以使线程阻塞,可是它们仍是有很大的区别:

  1. wait()方法会使当前线程释放锁,而sleep()方法则不会。
    当调用wait()方法后,当前线程会暂停执行,并进入互斥锁的等待队列中,直到有线程调用了notify()或者notifyAll(),等待队列中的线程才会被唤醒,从新竞争锁。
    sleep()方法的调用须要指定等待的时间,它让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,可是它不会使线程释放锁,这意味其余线程在当前线程阻塞的时候,是不能进入获取锁,执行同步代码的。
  2. wait()只能在同步方法或者同步代码块中执行,而sleep()能够在任何地方执行。
  3. 使用wait()无需捕获异常,而使用sleep()则必须捕获。
  4. wait()是Object类的方法,而sleep是Thread的方法。

为何wait()、notify()、notifyAll()是定义在Object中,而不是Thread中?

wait()、notify()以及notifyAll()它们之间的联系是依靠互斥锁,也就同步锁(内置锁),咱们前面介绍过,每一个Java对象均可以用做一个实现同步的锁,因此这些方法是定义在Object中,而不是Thread中。

小结

“等待—通知”机制是一种很是广泛的线程间协做的方式,咱们在理解时能够利用生活中的例子去相似,就如上面的就医流程。上文中没有明显说明notify()和notifyAll()的区别,只是在图中标注了一下。咱们建议尽可能使用notifyAll(),notify() 是会随机地通知等待队列中的一个线程,在极端状况下可能会使某个线程一直处于阻塞状态不能去竞争获取锁致使线程“饥饿”;而 notifyAll() 会通知等待队列中的全部线程,即全部等待的线程都有机会去获取锁的使用权。

参考: [1]极客时间专栏王宝令《Java并发编程实战》 [2]Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械工业出版社,2016 [3]skywang12345.Java多线程系列--“基础篇”05之 线程等待与唤醒.https://www.cnblogs.com/skywang12345/p/3479224.html

相关文章
相关标签/搜索