求求你,别再用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,一经查实,立即删除!

相关文章

JavaScript--变量、作用域及内存(12)

// JS变量是松散型的(不强制类型)本质,决定了它只是在特定时间用于保存特定值的一个名字而已; // 由于不存在定义某个变量必须要保存何种数据类型值的规则,变量的值及其数据类型可以在脚本的生命周期内改变; 一 变量及作用域 1.基本类型和引用类型 1 // JS变量包含两种不同的数…

查看MYSQL数据库中所有用户及拥有权限

查看MYSQL数据库中所有用户mysql> SELECT DISTINCT CONCAT(User: ,user,,host,;) AS query FROM mysql.user; --------------------------------------- | query | --------------------------------------- | User: cactiuser%; …

c ++类成员函数_C ++编程中的数据成员和成员函数

c 类成员函数C 中的数据成员和成员函数 (Data members and Member functions in C) "Data Member" and "Member Functions" are the new names/terms for the members of a class, which are introduced in C programming language. “数据成员”和“成员函…

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

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

ntfs安全权限和共享权限的区别

ntfs安全权限和共享权限的区别 win xp 最大分区32G,最大文件大小4G. 共享权限是为网络用户设置的,NTFS权限是对文件夹设置的。用户对文件夹有什么权限就是看NTFS权限的设置。如果一个文件夹设置成共享,其具体的权限还是在NTFS权限上面设置的,…

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.的错误.在网友说在任务管理器上把所有…

kotlin 16进制_Kotlin程序将八进制数转换为十进制数

kotlin 16进制Given a number in octal number system format, we have to convert it into decimal number system format. 给定八进制系统格式的数字,我们必须将其转换为十进制系统格式。 Example: 例: Input:num 344Output:228在Kotlin中将八进制数…

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

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

SQL调用C# dll(第一中DLL,没使用强名称密匙,默认是 safe)

https://msdn.microsoft.com/zh-cn/library/ms345106(es-es).aspx 1、新建项目名称SQLDllTest,类代码如下,没有用Using引用其他类: (框架必须改为.NET3.5及3.5以下,因为SQL Server 2008只是支持.NET 3.5及一下&#xf…

Linux系统下启动MySQL的命令及相关知识

一、总结一下: 1.Linux系统下启动MySQL的命令: /ect/init.d/mysql start (前面为mysql的安装路径) 2.linux下重启mysql的命令: /ect/init.d/mysql restart (前面为mysql的安装路径) 3.linux下关闭mysql的命令: /ect/init.d/mysql …

线性代数向量乘法_标量乘法属性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个线程的线程池进行测试…

MySql常用命令总结

1:使用SHOW语句找出在服务器上当前存在什么数据库:mysql> SHOW DATABASES;2:2、创建一个数据库MYSQLDATAmysql> CREATE DATABASE MYSQLDATA;3:选择你所创建的数据库mysql> USE MYSQLDATA; (按回车键出现Database changed 时说明操作成功!)4:查看…

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

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

sql 数字减去null_减去两个16位数字| 8086微处理器

sql 数字减去nullProblem: Write a program to subtract two 16-bit numbers where starting address is 2000 and the numbers are at 3000 and 3002 memory address and store result into 3004 and 3006 memory address. 问题:编写一个程序以减去两个16位数字(起…

Java 解决采集UTF-8网页空格变成问号乱码

http://blog.csdn.net/bob007/article/details/27098875 使用此方法转换后,在列表中看到的正常,但是在详情页的文本框中查看到的就是 了,只好过滤掉所有的空格 html html.replaceAll(UTFSpace, " ");改为html html.replaceAll(UT…

linux中如何改IP

修改IP永久生效按以下方法vi /etc/sysconfig/network-scripts/ifcfg-eth0(eth0,第一块网卡,如果是第二块则为eth1)按如下修改ipDEVICEeth0(如果是第二块刚为eth1)BOOTPROTOstaticIPADDR192.168.0.11(改成要…

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

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

单位矩阵属性(I ^ k = I)| 使用Python的线性代数

Prerequisites: 先决条件: numpy.matmul( ) matrix multiplication numpy.matmul()矩阵乘法 Identity matrix 身份矩阵 In linear algebra, the identity matrix, of size n is the n n square matrix with ones on the main diagonal and zeros elsewhere. It is…