只會用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 度的房間裏,穿一件短袖就能夠了,爲何還要穿一件棉襖呢?

相關文章
相關標籤/搜索