工作多年还是只会用wait和notify?30分钟用案例告诉你有更好得选择

Condition 是 JDK 1.5 中提供的用来替代 wait 和 notify 的线程通讯方法,那么一定会有人问:为什么不能用 wait 和 notify 了? 哥们我用的好好的。老弟别着急,听我给你细说...

之所以推荐使用 Condition 而非 Object 中的 wait 和 notify 的原因有两个:

1、使用 notify 在极端环境下会造成线程“假死”;

2、Condition 性能更高。

接下来咱们就用代码和流程图的方式来演示上述的两种情况。

文章首发公众号:Java架构师联盟,每日更新技术好文

1.notify 线程“假死”

所谓的线程“假死”是指,在使用 notify 唤醒多个等待的线程时,却意外的唤醒了一个没有“准备好”的线程,从而导致整个程序进入了阻塞的状态不能继续执行。

以多线程编程中的经典案例生产者和消费者模型为例,我们先来演示一下线程“假死”的问题。

1.1 正常版本

在演示线程“假死”的问题之前,我们先使用 wait 和 notify 来实现一个简单的生产者和消费者模型,为了让代码更直观,我这里写一个超级简单的实现版本。我们先来创建一个工厂类,工厂类里面包含两个方法,一个是循环生产数据的(存入)方法,另一个是循环消费数据的(取出)方法,实现代码如下。

package com.test.notify;
​
/**
 * @author :biws
 * @date :Created in 2020/12/17 22:11
 * @description:工厂类,消费者和生产者通过调用工厂类实现生产/消费
 */
public 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 方法,实现代码如下:

package com.test.notify;
​
/**
 * @author :biws
 * @date :Created in 2020/12/17 22:12
 * @description:测试线程正常版本
 */
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();
 }
 }

执行结果如下:

只会用wait和notify?30分钟案例告诉你有更好得选择

从上述结果可以看出,生产者和消费者在循环交替的执行任务,场面非常和谐,是我们想要的正确结果。

1.2 线程“假死”版本

当只有一个生产者和一个消费者时,wait 和 notify 方法不会有任何问题,然而将生产者增加到两个时就会出现线程“假死”的问题了,程序的实现代码如下:

package com.test.notify;
​
/**
 * @author :biws
 * @date :Created in 2020/12/17 22:15
 * @description:线程假死问题
 * 当创建两个生产者得时候会出现什么情况?
 */
public class NotifyDemo2 {
 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();
 }
}

程序执行结果如下:

只会用wait和notify?30分钟案例告诉你有更好得选择

从以上结果可以看出,当我们将生产者的数量增加到 2 个时,就会造成线程“假死”阻塞执行的问题,当生产者 2 被唤醒又被阻塞之后,整个程序就不能继续执行了。

线程“假死”问题分析

我们先把以上程序的执行步骤标注一下,得到如下结果:

只会用wait和notify?30分钟案例告诉你有更好得选择

从上图可以看出:

当执行到第 ④ 步时,此时生产者为工作状态,而生产者 2 和消费者为等待状态

此时正确的做法应该是唤醒消费者进行消费,然后消费者消费完之后再唤醒生产者继续工作;

但此时生产者却错误的唤醒了生产者 2,而生产者 2 因为队列已经满了,所以自身并不具备继续执行的能力,因此就导致了整个程序的阻塞,流程图如下所示:

只会用wait和notify?30分钟案例告诉你有更好得选择

正确执行流程应该是这样的:

只会用wait和notify?30分钟案例告诉你有更好得选择

1.3 使用 Condition

为了解决线程的“假死”问题,我们可以使用 Condition 来尝试实现一下,Condition 是 JUC(java.util.concurrent)包下的类,需要使用 Lock 锁来创建,Condition 提供了 3 个重要的方法:

  • await:对应 wait 方法;

  • signal:对应 notify 方法;

  • signalAll: notifyAll 方法。

因为 Condition 可以创建多个等待集,以本文的生产者和消费者模型为例,我们可以使用两个等待集,一个用作消费者的等待和唤醒,另一个用来唤醒生产者,这样就不会出现生产者唤醒生产者的情况了(生产者只能唤醒消费者,消费者只能唤醒生产者)这样整个流程就不会“假死”了,它的执行流程如下图所示:

只会用wait和notify?30分钟案例告诉你有更好得选择

了解了它的基本流程之后,咱们来看具体的实现代码。

package com.test.notify;
​
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
/**
 * @author :biws
 * @date :Created in 2020/12/17 22:27
 * @description:基于Condition得工厂实现
 */
public 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);
 }
}

两个生产者和一个消费者的实现代码如下:

package com.test.notify;
​
/**
 * @author :biws
 * @date :Created in 2020/12/17 22:30
 * @description:处理假死问题执行结果
 */
public class NotifyDemoByCondition {
 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();
 }
}

程序的执行结果如下图所示:

只会用wait和notify?30分钟案例告诉你有更好得选择

这个效果怎么样,按部就班,谁也不干扰谁,一点点得执行,是不是很好,但是,再美好得背后,肯定有更觉大的危机,不信?接着往下看

2.性能问题

在上面我们演示 notify 会造成线程的“假死”问题的时候,那有的朋友可能会说:如果把 notify 换成 notifyAll 线程就不会“假死”了。岂不是更简单?

我不多说,直接代码执行大家看结果

工厂类我还是使用之前的Fctory代码,只不过把notify更改为notifyAll()方法

只会用wait和notify?30分钟案例告诉你有更好得选择

依旧是两个生产者加一个消费者

执行的结果如下图所示:

只会用wait和notify?30分钟案例告诉你有更好得选择

通过以上结果可以看出:当我们调用 notifyAll 时确实不会造成线程“假死”了,但会造成所有的生产者都被唤醒了,但因为待执行的任务只有一个,因此被唤醒的所有生产者中,只有一个会执行正确的工作,而另一个则是啥也不干,然后又进入等待状态,这种行为对于整个程序来说,无疑是多此一举,只会增加线程调度的开销,从而导致整个程序的性能下降

反观 Condition 的 await 和 signal 方法,即使有多个生产者,程序也只会唤醒一个有效的生产者进行工作,如下图所示:

只会用wait和notify?30分钟案例告诉你有更好得选择

生产者和生产者 2 依次会被交替的唤醒进行工作,所以这样执行时并没有任何多余的开销,从而相比于 notifyAll 而且整个程序的性能会提升不少。

总结

本文我们通过代码和流程图的方式演示了 wait 方法和 notify/notifyAll 方法的使用缺陷,它的缺陷主要有两个,一个是在极端环境下使用 notify 会造成程序“假死”的情况,另一个就是使用 notifyAll 会造成性能下降的问题,因此在进行线程通讯时,强烈建议使用 Condition 类来实现。

PS:有人可能会问为什么不用 Condition 的 signalAll 和 notifyAll 进行性能对比?而使用 signal 和 notifyAll 进行对比?我只想说,既然使用 signal 可以实现此功能,为什么还要使用 signalAll 呢?这就好比在有暖气的 25 度的房间里,穿一件短袖就可以了,为什么还要穿一件棉袄呢?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容