Condition
是 JDK 1.5 中提供的用来替代 wait
和 notify
的线程通信方法,那么必定会有人问:为何不能用 wait
和 notify
了? 哥们我用的好好的。老弟别着急,听我给你细说...java
之因此推荐使用 Condition
而非 Object
中的 wait
和 notify
的缘由有两个:git
notify
在极端环境下会形成线程“假死”;Condition
性能更高。接下来怎们就用代码和流程图的方式来演示上述的两种状况。github
所谓的线程“假死”是指,在使用 notify
唤醒多个等待的线程时,却意外的唤醒了一个没有“准备好”的线程,从而致使整个程序进入了阻塞的状态不能继续执行。编程
以多线程编程中的经典案例生产者和消费者模型为例,咱们先来演示一下线程“假死”的问题。多线程
在演示线程“假死”的问题以前,咱们先使用 wait
和 notify
来实现一个简单的生产者和消费者模型,为了让代码更直观,我这里写一个超级简单的实现版本。咱们先来建立一个工厂类,工厂类里面包含两个方法,一个是循环生产数据的(存入)方法,另外一个是循环消费数据的(取出)方法,实现代码以下。性能
/** * 工厂类,消费者和生产者经过调用工厂类实现生产/消费 */ class Factory { private int[] items = new int[1]; // 数据存储容器(为了演示方便,设置容量最多存储 1 个元素) private int size = 0; // 实际存储大小 /** * 生产方法 */ public synchronized void put() throws InterruptedException { // 循环生产数据 do { while (size == items.length) { // 注意不能是 if 判断 // 存储的容量已经满了,阻塞等待消费者消费以后唤醒 System.out.println(Thread.currentThread().getName() + " 进入阻塞"); this.wait(); System.out.println(Thread.currentThread().getName() + " 被唤醒"); } System.out.println(Thread.currentThread().getName() + " 开始工做"); items[0] = 1; // 为了方便演示,设置固定值 size++; System.out.println(Thread.currentThread().getName() + " 完成工做"); // 当生产队列有数据以后通知唤醒消费者 this.notify(); } while (true); } /** * 消费方法 */ public synchronized void take() throws InterruptedException { // 循环消费数据 do { while (size == 0) { // 生产者没有数据,阻塞等待 System.out.println(Thread.currentThread().getName() + " 进入阻塞(消费者)"); this.wait(); System.out.println(Thread.currentThread().getName() + " 被唤醒(消费者)"); } System.out.println("消费者工做~"); size--; // 唤醒生产者能够添加生产了 this.notify(); } while (true); } }
接下来咱们来建立两个线程,一个是生产者调用 put
方法,另外一个是消费者调用 take
方法,实现代码以下:this
public class NotifyDemo { public static void main(String[] args) { // 建立工厂类 Factory factory = new Factory(); // 生产者 Thread producer = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者"); producer.start(); // 消费者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消费者"); consumer.start(); } }
执行结果以下:
从上述结果能够看出,生产者和消费者在循环交替的执行任务,场面很是和谐,是咱们想要的正确结果。spa
当只有一个生产者和一个消费者时,wait
和 notify
方法不会有任何问题,然而将生产者增长到两个时就会出现线程“假死”的问题了,程序的实现代码以下:线程
public class NotifyDemo { public static void main(String[] args) { // 建立工厂方法(工厂类的代码不变,这里再也不复述) Factory factory = new Factory(); // 生产者 Thread producer = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者"); producer.start(); // 生产者 2 Thread producer2 = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者2"); producer2.start(); // 消费者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消费者"); consumer.start(); } }
程序执行结果以下:
从以上结果能够看出,当咱们将生产者的数量增长到 2 个时,就会形成线程“假死”阻塞执行的问题,当生产者 2 被唤醒又被阻塞以后,整个程序就不能继续执行了。3d
咱们先把以上程序的执行步骤标注一下,获得以下结果:
从上图能够看出:当执行到第 ④ 步时,此时生产者为工做状态,而生产者 2 和消费者为等待状态,此时正确的作法应该是唤醒消费着进行消费,而后消费者消费完以后再唤醒生产者继续工做;但此时生产者却错误的唤醒了生产者 2,而生产者 2 由于队列已经满了,因此自身并不具有继续执行的能力,所以就致使了整个程序的阻塞,流程图以下所示:
正确执行流程应该是这样的:
为了解决线程的“假死”问题,咱们可使用 Condition
来尝试实现一下,Condition
是 JUC(java.util.concurrent)包下的类,须要使用 Lock
锁来建立,Condition
提供了 3 个重要的方法:
await
:对应 wait
方法;signal
:对应 notify
方法;signalAll
: notifyAll
方法。Condition
的使用和 wait/notify
相似,也是先得到锁而后在锁中进行等待和唤醒操做,Condition
的基础用法以下:
// 建立 Condition 对象 Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); // 加锁 lock.lock(); try { // 业务方法.... // 1.进入等待状态 condition.await(); // 2.唤醒操做 condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); }
切记 Lock
的 lock.lock()
方法不能放入 try
代码中,若是 lock
方法在 try
代码块以内,可能因为其它方法抛出异常,致使在 finally
代码块中, unlock
对未加锁的对象解锁,它会调用 AQS
的 tryRelease
方法(取决于具体实现类),抛出 IllegalMonitorStateException
异常。
回到本文的主题,咱们若是使用 Condition
来实现线程的通信就能够避免程序的“假死”状况,由于 Condition
能够建立多个等待集,以本文的生产者和消费者模型为例,咱们可使用两个等待集,一个用作消费者的等待和唤醒,另外一个用来唤醒生产者,这样就不会出现生产者唤醒生产者的状况了(生产者只能唤醒消费者,消费者只能唤醒生产者)这样整个流程就不会“假死”了,它的执行流程以下图所示:
了解了它的基本流程以后,我们来看具体的实现代码。
基于 Condition
的工厂实现代码以下:
class FactoryByCondition { private int[] items = new int[1]; // 数据存储容器(为了演示方便,设置容量最多存储 1 个元素) private int size = 0; // 实际存储大小 // 建立 Condition 对象 private Lock lock = new ReentrantLock(); // 生产者的 Condition 对象 private Condition producerCondition = lock.newCondition(); // 消费者的 Condition 对象 private Condition consumerCondition = lock.newCondition(); /** * 生产方法 */ public void put() throws InterruptedException { // 循环生产数据 do { lock.lock(); while (size == items.length) { // 注意不能是 if 判断 // 生产者进入等待 System.out.println(Thread.currentThread().getName() + " 进入阻塞"); producerCondition.await(); System.out.println(Thread.currentThread().getName() + " 被唤醒"); } System.out.println(Thread.currentThread().getName() + " 开始工做"); items[0] = 1; // 为了方便演示,设置固定值 size++; System.out.println(Thread.currentThread().getName() + " 完成工做"); // 唤醒消费者 consumerCondition.signal(); try { } finally { lock.unlock(); } } while (true); } /** * 消费方法 */ public void take() throws InterruptedException { // 循环消费数据 do { lock.lock(); while (size == 0) { // 消费者阻塞等待 consumerCondition.await(); } System.out.println("消费者工做~"); size--; // 唤醒生产者 producerCondition.signal(); try { } finally { lock.unlock(); } } while (true); } }
两个生产者和一个消费者的实现代码以下:
public class NotifyDemo { public static void main(String[] args) { FactoryByCondition factory = new FactoryByCondition(); // 生产者 Thread producer = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者"); producer.start(); // 生产者 2 Thread producer2 = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者2"); producer2.start(); // 消费者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消费者"); consumer.start(); } }
程序的执行结果以下图所示:
从上述结果能够看出,当使用 Condition
时,生产者、消费者、生产者 2 会一直交替循环执行,执行结果符合咱们的预期。
在上面咱们演示 notify
会形成线程的“假死”问题的时候,必定有朋友会想到,若是把 notify
换成 notifyAll
线程就不会“假死”了。
这样作法确实能够解决线程“假死”的问题,但同时会到来新的性能问题,空说无凭,直接上代码展现。
如下是使用 wait
和 notifyAll
改进后的代码:
/** * 工厂类,消费者和生产者经过调用工厂类实现生产/消费功能. */ class Factory { private int[] items = new int[1]; // 数据存储容器(为了演示方便,设置容量最多存储 1 个元素) private int size = 0; // 实际存储大小 /** * 生产方法 * @throws InterruptedException */ public synchronized void put() throws InterruptedException { // 循环生产数据 do { while (size == items.length) { // 注意不能是 if 判断 // 存储的容量已经满了,阻塞等待消费者消费以后唤醒 System.out.println(Thread.currentThread().getName() + " 进入阻塞"); this.wait(); System.out.println(Thread.currentThread().getName() + " 被唤醒"); } System.out.println(Thread.currentThread().getName() + " 开始工做"); items[0] = 1; // 为了方便演示,设置固定值 size++; System.out.println(Thread.currentThread().getName() + " 完成工做"); // 唤醒全部线程 this.notifyAll(); } while (true); } /** * 消费方法 * @throws InterruptedException */ public synchronized void take() throws InterruptedException { // 循环消费数据 do { while (size == 0) { // 生产者没有数据,阻塞等待 System.out.println(Thread.currentThread().getName() + " 进入阻塞(消费者)"); this.wait(); System.out.println(Thread.currentThread().getName() + " 被唤醒(消费者)"); } System.out.println("消费者工做~"); size--; // 唤醒全部线程 this.notifyAll(); } while (true); } }
依旧是两个生产者加一个消费者,实现代码以下:
public static void main(String[] args) { Factory factory = new Factory(); // 生产者 Thread producer = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者"); producer.start(); // 生产者 2 Thread producer2 = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者2"); producer2.start(); // 消费者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消费者"); consumer.start(); }
执行的结果以下图所示:
经过以上结果能够看出:当咱们调用 notifyAll
时确实不会形成线程“假死”了,但会形成全部的生产者都被唤醒了,但由于待执行的任务只有一个,所以被唤醒的全部生产者中,只有一个会执行正确的工做,而另外一个则是啥也不干,而后又进入等待状态,这就行为对于整个程序来讲,无疑是画蛇添足,只会增长线程调度的开销,从而致使整个程序的性能降低 。
反观 Condition
的 await
和 signal
方法,即便有多个生产者,程序也只会唤醒一个有效的生产者进行工做,以下图所示:
生产者和生产者 2 依次会被交替的唤醒进行工做,因此这样执行时并无任何多余的开销,从而相比于 notifyAll
而言整个程序的性能会提高很多。
本文咱们经过代码和流程图的方式演示了 wait
方法和 notify/notifyAll
方法的使用缺陷,它的缺陷主要有两个,一个是在极端环境下使用 notify
会形成程序“假死”的状况,另外一个就是使用 notifyAll
会形成性能降低的问题,所以在进行线程通信时,强烈建议使用 Condition
类来实现。
PS:有人可能会问为何不用 Condition 的 signalAll 和 notifyAll 进行性能对比?而使用 signal 和 notifyAll 进行对比?我只想说,既然使用 signal 能够实现此功能,为何还要使用 signalAll 呢?这就比如在有暖气的 25 度的房间里,穿一件短袖就能够了,为何还要穿一件棉袄呢?
关注公众号「Java中文社群」查看更多干货。查看 Github 发现更多精彩: https://github.com/vipstone/a...