求求你,别再用wait和notify了!

作者 | 王磊

来源 | 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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/545126.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一文学搞懂阿里开源的微服务新贵Nacos!

正式开始之前我们先来了解一下什么是 Nacos?Nacos 是阿里的一个开源产品,它是针对微服务架构中的 「服务发现」、「配置管理」、「服务治理」的综合性解决方案。官网给出的回答:“Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组…

The connection to adb is down, and a severe error has occured.

转自:http://blog.csdn.net/yu413854285/article/details/7559333 (感谢原文作者,问题解决) 启动android模拟器时.有时会报The connection to adb is down, and a severe error has occured.的错误.在网友说在任务管理器上把所有…

线程池的7种创建方式,强烈推荐你用它...

作者 | 王磊来源 | Java中文社群(ID:javacn666)转载请联系授权(微信ID:GG_Stone)根据摩尔定律所说:集成电路上可容纳的晶体管数量每 18 个月翻一番,因此 CPU 上的晶体管数量会越来越…

线性代数向量乘法_标量乘法属性1 | 使用Python的线性代数

线性代数向量乘法Prerequisite: Linear Algebra | Defining a Vector 先决条件: 线性代数| 定义向量 Linear algebra is the branch of mathematics concerning linear equations by using vector spaces and through matrices. In other words, a vector is a mat…

Synchronized 的 8 种使用场景!

blog.csdn.net/x541211190/article/details/106272922简介本文将介绍8种同步方法的访问场景,我们来看看这8种情况下,多线程访问同步方法是否还是线程安全的。这些场景是多线程编程中经常遇到的,而且也是面试时高频被问到的问题,所…

Python的threadpool模块

2019独角兽企业重金招聘Python工程师标准>>> Python的threadpool模块 这是一个使用python实现的线程池库。 安装 pip install threadpool 文档 http://gashero.yeax.com/?p44 http://www.chrisarndt.de/projects/threadpool/ 测试 使用一个20个线程的线程池进行测试…

硬核Redis总结,看这篇就够了!

高清思维导图已同步Git:https://github.com/SoWhat1412/xmindfile总感觉哪里不对,但是又说不上来1、基本类型及底层实现1.1、String用途:适用于简单key-value存储、setnx key value实现分布式锁、计数器(原子性)、分布式全局唯一ID。底层&…

文件写入的6种方法,这种方法性能最好

作者 | 王磊来源 | Java中文社群(ID:javacn666)转载请联系授权(微信ID:GG_Stone)在 Java 中操作文件的方法本质上只有两种:字符流和字节流,而字节流和字符流的实现类又有很多&#x…

JDK 16 即将发布,新特性速览!

你还能追上 Java 的更新速度吗?当开发者深陷 Java 8 版本之际,这边下一版本 Java 16 有了最新的消息,与 Java 15 一样,作为短期版本,Oracle 仅提供 6 个月的支持。根据发布计划,JDK 16 将在 12 月 10 日和 …

最牛逼的 Java 项目实战,没有之一!

想要成长为高级开发,掌握更多层面的技术,兼顾深度和广度是毋庸置疑的。你肯定认为,我要认真努力的学习技术,丰富自己的技术栈,然后就可以成为一个优秀的高级开发了。但当你真正去学习之后就会发现,技术栈异…

定时任务的实现原理,看完就能手撸一个!

一、摘要在很多业务的系统中,我们常常需要定时的执行一些任务,例如定时发短信、定时变更数据、定时发起促销活动等等。在上篇文章中,我们简单的介绍了定时任务的使用方式,不同的架构对应的解决方案也有所不同,总结起来…

Spring Boot集成Redis,这个坑把我害惨了!

最近项目中使用SpringBoot集成Redis,踩到了一个坑:从Redis中获取数据为null,但实际上Redis中是存在对应的数据的。是什么原因导致此坑的呢?本文就带大家从SpringBoot集成Redis、所踩的坑以及自动配置源码分析来学习一下SpringBoot…

数据分析告诉你为什么Apple Watch会大卖?

摘要: 不管是无敌创意还是无聊鸡肋,苹果手表还是来了。眼下它上市在即,将率先登陆9个国家或地区——包括中国。根据凌晨发布会上公布的内容,Apple Watch采用全新的压感触屏和蓝宝石镜面,能够记录健康数据、同步手机信息 ...不管是…

putc函数_C语言中的putc()函数与示例

putc函数C语言中的putc()函数 (putc() function in C) The putc() function is defined in the <stdio.h> header file. putc()函数在<stdio.h>头文件中定义。 Prototype: 原型&#xff1a; int putc(const char ch, FILE *filename);Parameters: const char ch,…

编程中的21个坑,你占几个?

前言最近看了某客时间的《Java业务开发常见错误100例》&#xff0c;再结合平时踩的一些代码坑&#xff0c;写写总结&#xff0c;希望对大家有帮助&#xff0c;感谢阅读~1. 六类典型空指针问题包装类型的空指针问题级联调用的空指针问题Equals方法左边的空指针问题ConcurrentHas…

Mybatis使用的9种设计模式,真是太有用了

crazyant.net/2022.html虽然我们都知道有26个设计模式&#xff0c;但是大多停留在概念层面&#xff0c;真实开发中很少遇到&#xff0c;Mybatis源码中使用了大量的设计模式&#xff0c;阅读源码并观察设计模式在其中的应用&#xff0c;能够更深入的理解设计模式。Mybatis至少遇…

Java 生成随机数的 5 种方式,你知道几种?

1. Math.random() 静态方法产生的随机数是 0 - 1 之间的一个 double&#xff0c;即 0 < random < 1。使用&#xff1a;for (int i 0; i < 10; i) {System.out.println(Math.random()); }结果&#xff1a;0.3598613895606426 0.2666778145365811 0.25090731064243355 …

MySQL为Null会导致5个问题,个个致命!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;正式开始之前&#xff0c;我们先来看下 MySQL 服务器的配置和版本号信息&#xff0c;如下图所示&#xff1a;“兵马未动粮草…

Spring Boot 解决跨域问题的 3 种方案!

作者 | telami来源 | telami.cn/2019/springboot-resolve-cors前后端分离大势所趋&#xff0c;跨域问题更是老生常谈&#xff0c;随便用标题去google或百度一下&#xff0c;能搜出一大片解决方案&#xff0c;那么为啥又要写一遍呢&#xff0c;不急往下看。问题背景&#xff1a;…

SpringBoot集成Google开源图片处理框架,贼好用!

1、序在实际开发中&#xff0c;难免会对图片进行一些处理&#xff0c;比如图片压缩之类的&#xff0c;而其中压缩可能就是最为常见的。最近&#xff0c;我就被要求实现这个功能&#xff0c;原因是客户那边嫌速度过慢。借此机会&#xff0c;今儿就给大家介绍一些一下我做这个功能…