生产者消费者模型是咱们学习多线程知识的一个经典案例,一个典型的生产者消费者模型以下:java
public void produce() { synchronized (this) { while (mBuf.isFull()) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } mBuf.add(); notifyAll(); } } public void consume() { synchronized (this) { while (mBuf.isEmpty()) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } mBuf.remove(); notifyAll(); } }
这段代码很容易引伸出来两个问题:一个是wait()方法外面为何是while循环而不是if判断,另外一个是结尾处的为何要用notifyAll()方法,用notify()行吗。多线程
不少人在回答第二个问题的时候会想固然的说notify()是唤醒一个线程,notifyAll()是唤醒所有线程,可是唤醒而后呢,不论是notify()仍是notifyAll(),最终拿到锁的只会有一个线程,那它们到底有什么区别呢?ide
其实这是一个对象内部锁的调度问题,要回答这两个问题,首先咱们要明白java中对象锁的模型,JVM会为一个使用内部锁(synchronized)的对象维护两个集合,Entry Set和Wait Set,也有人翻译为锁池和等待池,意思基本一致。学习
对于Entry Set:若是线程A已经持有了对象锁,此时若是有其余线程也想得到该对象锁的话,它只能进入Entry Set,而且处于线程的BLOCKED状态。测试
对于Wait Set:若是线程A调用了wait()方法,那么线程A会释放该对象的锁,进入到Wait Set,而且处于线程的WAITING状态。this
还有须要注意的是,某个线程B想要得到对象锁,通常状况下有两个先决条件,一是对象锁已经被释放了(如曾经持有锁的前任线程A执行完了synchronized代码块或者调用了wait()方法等等),二是线程B已处于RUNNABLE状态。spa
那么这两类集合中的线程都是在什么条件下能够转变为RUNNABLE呢?线程
对于Entry Set中的线程,当对象锁被释放的时候,JVM会唤醒处于Entry Set中的某一个线程,这个线程的状态就从BLOCKED转变为RUNNABLE。翻译
对于Wait Set中的线程,当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某一个线程,这个线程的状态就从WAITING转变为RUNNABLE;或者当notifyAll()方法被调用时,Wait Set中的所有线程会转变为RUNNABLE状态。全部Wait Set中被唤醒的线程会被转移到Entry Set中。code
而后,每当对象的锁被释放后,那些全部处于RUNNABLE状态的线程会共同去竞争获取对象的锁,最终会有一个线程(具体哪个取决于JVM实现,队列里的第一个?随机的一个?)真正获取到对象的锁,而其余竞争失败的线程继续在Entry Set中等待下一次机会。
有了这些知识点做为基础,上述的两个问题就能解释的清了。
首先来看第一个问题,咱们在调用wait()方法的时候,内心想的确定是由于当前方法不知足咱们指定的条件,所以执行这个方法的线程须要等待直到其余线程改变了这个条件而且作出了通知。那么为何要把wait()方法放在循环而不是if判断里呢,其实答案显而易见,由于wait()的线程永远不能肯定其余线程会在什么状态下notify(),因此必须在被唤醒、抢占到锁而且从wait()方法退出的时候再次进行指定条件的判断,以决定是知足条件往下执行呢仍是不知足条件再次wait()呢。
就像在本例中,若是只有一个生产者线程,一个消费者线程,那实际上是能够用if代替while的,由于线程调度的行为是开发者能够预测的,生产者线程只有可能被消费者线程唤醒,反之亦然,所以被唤醒时条件始终知足,程序不会出错。可是这种状况只是多线程状况下极为简单的一种,更广泛的是多个线程生产,多个线程消费,那么就极有可能出现唤醒生产者的是另外一个生产者或者唤醒消费者的是另外一个消费者,这样的状况下用if就必然会现相似过分生产或者过分消费的状况了,典型如IndexOutOfBoundsException的异常。因此全部的java书籍都会建议开发者永远都要把wait()放到循环语句里面。
而后来看第二个问题,既然notify()和notifyAll()最终的结果都是只有一个线程能拿到锁,那唤醒一个和唤醒多个有什么区别呢?
耐心看下面这个两个生产者两个消费者的场景,若是咱们代码中使用了notify()而非notifyAll(),假设消费者线程1拿到了锁,判断buffer为空,那么wait(),释放锁;而后消费者2拿到了锁,一样buffer为空,wait(),也就是说此时Wait Set中有两个线程;而后生产者1拿到锁,生产,buffer满,notify()了,那么可能消费者1被唤醒了,可是此时还有另外一个线程生产者2在Entry Set中盼望着锁,而且最终抢占到了锁,但由于此时buffer是满的,所以它要wait();而后消费者1拿到了锁,消费,notify();这时就有问题了,此时生产者2和消费者2都在Wait Set中,buffer为空,若是唤醒生产者2,没毛病;但若是唤醒了消费者2,由于buffer为空,它会再次wait(),这就尴尬了,万一辈子产者1已经退出再也不生产了,没有其余线程在竞争锁了,只有生产者2和消费者2在Wait Set中互相等待,那传说中的死锁就发生了。
但若是你把上述例子中的notify()换成notifyAll(),这样的状况就不会再出现了,由于每次notifyAll()都会使其余等待的线程从Wait Set进入Entry Set,从而有机会得到锁。
其实说了这么多,一句话解释就是之因此咱们应该尽可能使用notifyAll()的缘由就是,notify()很是容易致使死锁。固然notifyAll并不必定都是优势,毕竟一次性将Wait Set中的线程都唤醒是一笔不菲的开销,若是你能handle你的线程调度,那么使用notify()也是有好处的。
最后我把完整的测试代码放出来,供你们参考:
import java.util.ArrayList; import java.util.List; public class Something { private Buffer mBuf = new Buffer(); public void produce() { synchronized (this) { while (mBuf.isFull()) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } mBuf.add(); notifyAll(); } } public void consume() { synchronized (this) { while (mBuf.isEmpty()) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } mBuf.remove(); notifyAll(); } } private class Buffer { private static final int MAX_CAPACITY = 1; private List innerList = new ArrayList<>(MAX_CAPACITY); void add() { if (isFull()) { throw new IndexOutOfBoundsException(); } else { innerList.add(new Object()); } System.out.println(Thread.currentThread().toString() + " add"); } void remove() { if (isEmpty()) { throw new IndexOutOfBoundsException(); } else { innerList.remove(MAX_CAPACITY - 1); } System.out.println(Thread.currentThread().toString() + " remove"); } boolean isEmpty() {