首頁>技術>

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 度的房間裡,穿一件短袖就可以了,為什麼還要穿一件棉襖呢?

19
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 一鼓作氣學會“一致性雜湊”,就靠這 18 張圖了