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

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

之因此推荐使用 Condition 而非 Object 中的 wait 和 notify 的缘由有两个:编程

一、使用 notify 在极端环境下会形成线程“假死”;多线程

二、Condition 性能更高。架构

接下来我们就用代码和流程图的方式来演示上述的两种状况。性能

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

1.notify 线程“假死”

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

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

1.1 正常版本

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

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 度的房间里,穿一件短袖就能够了,为何还要穿一件棉袄呢?

相关文章
相关标签/搜索