作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

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

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

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

  2. Condition 性能更高。

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

1.notify 线程“假死”

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

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

1.1 正常版本

在演示线程“假死”的问题之前,我们先使用 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 方法,实现代码如下:

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();}
}

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

1.2 线程“假死”版本

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

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();// 生产者 2Thread 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 被唤醒又被阻塞之后,整个程序就不能继续执行了。

线程“假死”问题分析

我们先把以上程序的执行步骤标注一下,得到如下结果:从上图可以看出:当执行到第 ④ 步时,此时生产者为工作状态,而生产者 2 和消费者为等待状态,此时正确的做法应该是唤醒消费着进行消费,然后消费者消费完之后再唤醒生产者继续工作;但此时生产者却错误的唤醒了生产者 2,而生产者 2 因为队列已经满了,所以自身并不具备继续执行的能力,因此就导致了整个程序的阻塞,流程图如下所示:

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

1.3 使用 Condition

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

  • await:对应 wait 方法;

  • signal:对应 notify 方法;

  • signalAllnotifyAll 方法。

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.lock() 方法不能放入 try 代码中,如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中, unlock 对未加锁的对象解锁,它会调用 AQStryRelease 方法(取决于具体实现类),抛出 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();// 生产者 2Thread 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 会一直交替循环执行,执行结果符合我们的预期。

2.性能问题

在上面我们演示 notify 会造成线程的“假死”问题的时候,一定有朋友会想到,如果把 notify 换成 notifyAll 线程就不会“假死”了。

这样做法确实可以解决线程“假死”的问题,但同时会到来新的性能问题,空说无凭,直接上代码展示。

以下是使用 waitnotifyAll 改进后的代码:

/*** 工厂类,消费者和生产者通过调用工厂类实现生产/消费功能.*/
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();// 生产者 2Thread 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 度的房间里,穿一件短袖就可以了,为什么还要穿一件棉袄呢?


往期推荐

求求你,不要再使用!=null判空了!

2020-12-01

提高生产力,最全 MyBatisPlus 讲解!

2020-12-10

2020年终总结:新的“开始”

2020-12-11

关注我,每天陪你进步一点点!

求求你,别再用wait和notify了!相关推荐

  1. 求求你们别再去培训机构学假的人工智能了,培训机构的人工智能培训的“坑”

    自从人工智能走入大家的视线,到今年新公布的七大"新基建",人工智能榜上有名.人工智能行业发展可以说是热火朝天,当然,导致了一些不合规的培训机构打着"打着人工智能的旗号&q ...

  2. 通过某某大厂面试,拿到offer,求求你们别再这么唬人了

    通过某某大厂面试,拿到offer,求求你们别再这么唬人了 真的是无语了,逛论坛经常会刷到一些拿到大厂offer然后分享面试题的.通常这些面试都是好几轮的,这些问题也非 常的难,没有深入学习或者没什么经 ...

  3. 求求你别再“从入门到放弃了”,贵在坚持

    自从参加了写作课,每次都说想要来个爆款,激发下自己:每次都下定决心,需要日更(不存在的):每次没有日更的时候,总觉得心里有负罪感,那可是花了时间.金钱和精力的,好尴尬呀! 有了一定的粉丝,也会被催更说 ...

  4. 求求你别再写上千行的类了,试试这些牛逼的重构技巧吧

    欢迎关注方志朋的博客,回复"666"获面试宝典 来源:juejin.cn/post/6844904038383747086 答应我,别再写上千行的类了好吗 最近在对已有项目进行扩展 ...

  5. 面试被问烂的 Spring IOC(求求你别再问了)

    点击上方"方志朋",选择"置顶或者星标" 你的关注意义重大! 作者:莫那·鲁道 链接:http://thinkinjava.cn 广义的 IOC IoC(Inv ...

  6. 求求你别再写上千行的类了,试试 IDEA 这些牛逼的重构技巧吧

    来自:掘金,作者:围军儿 链接:https://juejin.cn/post/6844904038383747086 答应我,别再写上千行的类了好吗 最近在对已有项目进行扩展的时候,发现要改动的一个类 ...

  7. 求求你别再写上千行的类了,试试 IDEA 这些牛逼的重构技巧吧!

    原文:juejin.cn/post/6844904038383747086 答应我,别再写上千行的类了好吗 最近在对已有项目进行扩展的时候,发现要改动的一个类它长766行,开放了近40个public接 ...

  8. 公司这套架构统一处理 try...catch 这么香,求求你不要再满屏写了,再发现扣绩效!...

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 作者:小李子说程序 www.toutiao.com/i68 ...

  9. 求求你别再用 System.out.println 了!!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 来源:my.oschina.net/yuchener/bl ...

最新文章

  1. 基于GAN的单目图像3D物体重建(纹理和形状)
  2. 直播|深大周池:跨数据中心的高效大图处理问题研究
  3. CCF NOI1134 奶牛晒衣服
  4. ASP.NET用户控件的创建和使用
  5. mysql和Oracle 备份表
  6. 计算特征数据matlab代码,科学网—MATLAB特征提取代码 - 蒋样明的博文
  7. 今日arXiv精选 | 31篇EMNLP 2021最新论文
  8. Java的Object类
  9. 测试人员报BUG的正确姿势
  10. AI在中国,还没到抢切蛋糕的时候
  11. 大二数据库期末考试大题
  12. 去除浏览器输入框等的边框(包括手机浏览器)
  13. 【重构与模式】6.1用Creation Method替换构造函数
  14. 吃一堑长一智,作为程序员的我们记住这几点,2021年Android开发实战
  15. centOs安装 ruby环境
  16. Web—13-判断网站请求来自手机还是pc浏览器
  17. 华东师范大学2020计算机研究生招生简章,2020华东师范大学计算机考研大纲
  18. ffmpeg水印制作
  19. DDD之一年级小学生作业辅导
  20. 2021年遥感类SCI期刊中科院分区排名与影响因子汇总

热门文章

  1. python 命名空间冲突_python-命名空间
  2. MongoDB Shell操作
  3. Redis 核心技术与实战
  4. 想提高用户访问的响应速度和成功率还不赶快学习CDN
  5. java8-02-Stream-API
  6. PHPWeb开发入门体验学习笔记
  7. SpriteBuilder物理对象的父子关系
  8. Python正则表达式指南上半部
  9. 页面增加html,为静态页面HTML增加session功能
  10. 前端多图片上传怎么控制顺序_Web前端经典面试题有哪些 如何能走向高薪之路...