求求你,别再用wait和notify了!
Condition 是 JDK 1.5 中提供的用来替代 wait 和 notify 的线程通讯方法,那么一定会有人问:为什么不能用 wait 和 notify 了? 哥们我用的好好的。老弟别着急,听我给你细说...
之所以推荐使用 Condition 而非 Object 中的 wait 和 notify 的原因有两个:
- 使用 notify在极端环境下会造成线程“假死”;
- 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 线程“假死”版本
当只有一个生产者和一个消费者时,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 被唤醒又被阻塞之后,整个程序就不能继续执行了。
线程“假死”问题分析
我们先把以上程序的执行步骤标注一下,得到如下结果:

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

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

1.3 使用 Condition
为了解决线程的“假死”问题,我们可以使用 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.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 会一直交替循环执行,执行结果符合我们的预期。
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/algorithm
求求你,别再用wait和notify了!的更多相关文章
- Java再学习——sleep(), wait(), notify(), notifyAll()
		首先一点就是Thread.sleep(long millis)方法是Thread类的静态方法,其他三个wait(), notify()和notifyAll()都是Object类的方法. sleep(l ... 
- 面试被问烂的 Spring IOC(求求你别再问了)
		广义的 IOC IoC(Inversion of Control) 控制反转,即"不用打电话过来,我们会打给你". 两种实现: 依赖查找(DL)和依赖注入(DI). IOC 和 D ... 
- 求求你们不要再用 RSA 私钥加密公钥解密了,这非常不安全!
		最近经常在网上看到有人说巨硬的 CNG(Cryptography Next Generation 即下一代加密技术) 只提供 RSA 公钥加密私钥解密,没有提供 RSA 私钥加密公钥解密,他们要自己封 ... 
- 生产者与消费者(一)---wait与notify
		生产者消费者问题是研究多线程程序时绕不开的经典问题之一,它描述是有一块缓冲区作为仓库,生产者可以将产品放入仓库,消费者则可以从仓库中取走产品.解决生产者/消费者问题的方法可分为两类: (1)采用某种机 ... 
- 通过两个小栗子来说说Java的sleep、wait、notify、notifyAll的用法
		线程是计算程序运行的最小载体,由于单个单核CPU的硬件水平发展到了一定的瓶颈期,因此就出现了多核多CPU的情况,直接就导致程序员多线程编程的复杂.由此可见线程对于高性能开发的重要性. 那么线程在计算机 ... 
- Effective Java 第三版——81. 优先使用并发实用程序替代wait和notify
		Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ... 
- 再谈java线程
		线程状态 描述 当线程被创建并启动之后,它既不是已启动就进入到了执行状态,也不是一直处于执行状态.在线程的声明周期中有六中状态. java api中java.lang.Thread.State这个枚举 ... 
- Xamarin.Android通知详解
		一.发送通知的机制 在日常的app应用中经常需要使用通知,因为服务.广播后台活动如果有事件需要通知用户,则需要通过通知栏显示,而在Xamarin.Android下的通知需要获取Notification ... 
- Xamarin.Android开发实践(六)
		Xamarin.Android通知详解 一.发送通知的机制 在日常的app应用中经常需要使用通知,因为服务.广播后台活动如果有事件需要通知用户,则需要通过通知栏显示,而在Xamarin.Android ... 
随机推荐
- js,昨天的日期
			var mydate = new Date(); var yester = mydate-24*60*60*1000; var yesterday = new Date(); yesterday.se ... 
- LeetCode 中等题解(1)
			16 最接近的三数之和 Question 给定一个包括 n 个整数的数组 nums 和 一个目标值 target.找出 nums 中的三个整数,使得它们的和与 target 最接近.返回这三个数的和. ... 
- CTF-WEB-HCTF 2018 Warmup
			题目链接 攻防世界-Warmup 解题思路 [原题复现]HCTF 2018 Warmup(文件包含) 
- 如何制作C语言基本数据类型的思维导图
			在使用C语言编写程序时,数据类型是一个非常重要的内容,任何一个不被重视的数据错误都会使编译器无法翻译,导致程序报错. 使用思维导图来梳理各个数据类型是一个很有效的记忆方法,接下来就为大家展示一下我用i ... 
- 初学者也能轻松做出好Beat:FPC鼓机使用教程
			如果我们想用FL Studio制作一个鼓的声部,这时水果自带的鼓机FPC简直就是我们初学者的福音.因为它的操作比较简单,自带的鼓谱也很丰富,而且我们还可以对鼓的音色做细致的调整,或者是使用自己的采样替 ... 
- guitar pro系列教程(二十七):Guitar Pro教程之理解记谱法
			前面的章节我们讲解了很多关于Guitar Pro'的功能使用,今天小编还是采用图文结合的方式为大家讲解它的理解记谱法,对于很多新人来说,在我们看谱之前,我们肯定要先熟悉他的一些功能如何使用以及一些关于 ... 
- zabbix的搭建及操作(4)实现邮件,钉钉,微信报警
			实现邮件报警 网页版邮箱中开启 POP3/SMTP/IMAP 生成授权码并记录 Server端安装配置邮件服务器 1.Yum安装邮件服务器 yum -y install mailx dos2unix ... 
- python基础之条件语句
			检查相等和不等 多个检查条件 age1 = 22 age2 = 19 s1 = age1 > 21 and age2 > 19 print(s1) s2 = age1 > 21 or ... 
- nginx介绍及常用功能
			什么是nginx nginx跟Apache一样,是一个web服务器(网站服务器),通过HTTP协议提供各种网络服务. Apache:重量级的,不支持高并发的服务器.在Apache上运行数以万计的并发访 ... 
- C语言讲义——指针(pointer)
			指针是C语言最重要的特性之一, 也是最容易被误解的特性之一. 现代计算机把内存分割为字节(Byte), 每个字节都有唯一的地址(Address), 如果内存中有n个字节,可以把地址看做0~n-1的数. ... 
