工作多年還是只會用wait和notify?30分鐘用案例告訴你有更好得選擇

Condition 是 JDK 1.5 中提供的用來替代 wait 和 notify 的線程通訊方法,那么一定會有人問:為什么不能用 wait 和 notify 了? 哥們我用的好好的。老弟別著急,聽我給你細說...

之所以推薦使用 Condition 而非 Object 中的 wait 和 notify 的原因有兩個:

1、使用 notify 在極端環(huán)境下會造成線程“假死”;

2、Condition 性能更高。

接下來咱們就用代碼和流程圖的方式來演示上述的兩種情況。

文章首發(fā)公眾號:Java架構師聯盟,每日更新技術好文

1.notify 線程“假死”

所謂的線程“假死”是指,在使用 notify 喚醒多個等待的線程時,卻意外的喚醒了一個沒有“準備好”的線程,從而導致整個程序進入了阻塞的狀態(tài)不能繼續(xù)執(zhí)行。

以多線程編程中的經典案例生產者和消費者模型為例,我們先來演示一下線程“假死”的問題。

1.1 正常版本

在演示線程“假死”的問題之前,我們先使用 wait 和 notify 來實現一個簡單的生產者和消費者模型,為了讓代碼更直觀,我這里寫一個超級簡單的實現版本。我們先來創(chuàng)建一個工廠類,工廠類里面包含兩個方法,一個是循環(huán)生產數據的(存入)方法,另一個是循環(huán)消費數據的(取出)方法,實現代碼如下。

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 {
 // 循環(huán)生產數據
 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 {
 // 循環(huán)消費數據
 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);
 }
 }

接下來我們來創(chuàng)建兩個線程,一個是生產者調用 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) {
 // 創(chuàng)建工廠類
 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();
 }
 }

執(zhí)行結果如下:

只會用wait和notify?30分鐘案例告訴你有更好得選擇

從上述結果可以看出,生產者和消費者在循環(huán)交替的執(zhí)行任務,場面非常和諧,是我們想要的正確結果。

1.2 線程“假死”版本

當只有一個生產者和一個消費者時,wait 和 notify 方法不會有任何問題,然而將生產者增加到兩個時就會出現線程“假死”的問題了,程序的實現代碼如下:

package com.test.notify;
?
/**
 * @author :biws
 * @date :Created in 2020/12/17 22:15
 * @description:線程假死問題
 * 當創(chuàng)建兩個生產者得時候會出現什么情況?
 */
public class NotifyDemo2 {
 public static void main(String[] args) {
 // 創(chuàng)建工廠方法(工廠類的代碼不變,這里不再復述)
 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();
 }
}

程序執(zhí)行結果如下:

只會用wait和notify?30分鐘案例告訴你有更好得選擇

從以上結果可以看出,當我們將生產者的數量增加到 2 個時,就會造成線程“假死”阻塞執(zhí)行的問題,當生產者 2 被喚醒又被阻塞之后,整個程序就不能繼續(xù)執(zhí)行了。

線程“假死”問題分析

我們先把以上程序的執(zhí)行步驟標注一下,得到如下結果:

只會用wait和notify?30分鐘案例告訴你有更好得選擇

從上圖可以看出:

當執(zhí)行到第 ④ 步時,此時生產者為工作狀態(tài),而生產者 2 和消費者為等待狀態(tài)

此時正確的做法應該是喚醒消費者進行消費,然后消費者消費完之后再喚醒生產者繼續(xù)工作;

但此時生產者卻錯誤的喚醒了生產者 2,而生產者 2 因為隊列已經滿了,所以自身并不具備繼續(xù)執(zhí)行的能力,因此就導致了整個程序的阻塞,流程圖如下所示:

只會用wait和notify?30分鐘案例告訴你有更好得選擇

正確執(zhí)行流程應該是這樣的:

只會用wait和notify?30分鐘案例告訴你有更好得選擇

1.3 使用 Condition

為了解決線程的“假死”問題,我們可以使用 Condition 來嘗試實現一下,Condition 是 JUC(java.util.concurrent)包下的類,需要使用 Lock 鎖來創(chuàng)建,Condition 提供了 3 個重要的方法:

  • await:對應 wait 方法;

  • signal:對應 notify 方法;

  • signalAll: notifyAll 方法。

因為 Condition 可以創(chuàng)建多個等待集,以本文的生產者和消費者模型為例,我們可以使用兩個等待集,一個用作消費者的等待和喚醒,另一個用來喚醒生產者,這樣就不會出現生產者喚醒生產者的情況了(生產者只能喚醒消費者,消費者只能喚醒生產者)這樣整個流程就不會“假死”了,它的執(zhí)行流程如下圖所示:

只會用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;             // 實際存儲大小
 // 創(chuàng)建 Condition 對象
 private Lock lock = new ReentrantLock();
 // 生產者的 Condition 對象
 private Condition producerCondition = lock.newCondition();
 // 消費者的 Condition 對象
 private Condition consumerCondition = lock.newCondition();
?
 /**
 * 生產方法
 */
 public void put() throws InterruptedException {
 // 循環(huán)生產數據
 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 {
 // 循環(huán)消費數據
 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:處理假死問題執(zhí)行結果
 */
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();
 }
}

程序的執(zhí)行結果如下圖所示:

只會用wait和notify?30分鐘案例告訴你有更好得選擇

這個效果怎么樣,按部就班,誰也不干擾誰,一點點得執(zhí)行,是不是很好,但是,再美好得背后,肯定有更覺大的危機,不信?接著往下看

2.性能問題

在上面我們演示 notify 會造成線程的“假死”問題的時候,那有的朋友可能會說:如果把 notify 換成 notifyAll 線程就不會“假死”了。豈不是更簡單?

我不多說,直接代碼執(zhí)行大家看結果

工廠類我還是使用之前的Fctory代碼,只不過把notify更改為notifyAll()方法

只會用wait和notify?30分鐘案例告訴你有更好得選擇

依舊是兩個生產者加一個消費者

執(zhí)行的結果如下圖所示:

只會用wait和notify?30分鐘案例告訴你有更好得選擇

通過以上結果可以看出:當我們調用 notifyAll 時確實不會造成線程“假死”了,但會造成所有的生產者都被喚醒了,但因為待執(zhí)行的任務只有一個,因此被喚醒的所有生產者中,只有一個會執(zhí)行正確的工作,而另一個則是啥也不干,然后又進入等待狀態(tài),這種行為對于整個程序來說,無疑是多此一舉,只會增加線程調度的開銷,從而導致整個程序的性能下降

反觀 Condition 的 await 和 signal 方法,即使有多個生產者,程序也只會喚醒一個有效的生產者進行工作,如下圖所示:

只會用wait和notify?30分鐘案例告訴你有更好得選擇

生產者和生產者 2 依次會被交替的喚醒進行工作,所以這樣執(zhí)行時并沒有任何多余的開銷,從而相比于 notifyAll 而且整個程序的性能會提升不少。

總結

本文我們通過代碼和流程圖的方式演示了 wait 方法和 notify/notifyAll 方法的使用缺陷,它的缺陷主要有兩個,一個是在極端環(huán)境下使用 notify 會造成程序“假死”的情況,另一個就是使用 notifyAll 會造成性能下降的問題,因此在進行線程通訊時,強烈建議使用 Condition 類來實現。

PS:有人可能會問為什么不用 Condition 的 signalAll 和 notifyAll 進行性能對比?而使用 signal 和 notifyAll 進行對比?我只想說,既然使用 signal 可以實現此功能,為什么還要使用 signalAll 呢?這就好比在有暖氣的 25 度的房間里,穿一件短袖就可以了,為什么還要穿一件棉襖呢?

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容