Condition 是 JDK 1.5 中提供的用來替代 wait 和 notify 的執行緒通訊方法,那么一定會有人問:為什么不能用 wait 和 notify 了? 哥們我用的好好的,老弟別著急,聽我給你細說...
之所以推薦使用 Condition 而非 Object 中的 wait 和 notify 的原因有兩個:
1、使用 notify 在極端環境下會造成執行緒“假死”;
2、Condition 性能更高,
接下來咱們就用代碼和流程圖的方式來演示上述的兩種情況,
文章首發公眾號:Java架構師聯盟,每日更新技術好文
1.notify 執行緒“假死”
所謂的執行緒“假死”是指,在使用 notify 喚醒多個等待的執行緒時,卻意外的喚醒了一個沒有“準備好”的執行緒,從而導致整個程式進入了阻塞的狀態不能繼續執行,
以多執行緒編程中的經典案例生產者和消費者模型為例,我們先來演示一下執行緒“假死”的問題,
1.1 正常版本
在演示執行緒“假死”的問題之前,我們先使用 wait 和 notify 來實作一個簡單的生產者和消費者模型,為了讓代碼更直觀,我這里寫一個超級簡單的實作版本,我們先來創建一個工廠類,工廠類里面包含兩個方法,一個是回圈生產資料的(存入)方法,另一個是回圈消費資料的(取出)方法,實作代碼如下,
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();
}
}
執行結果如下:
從上述結果可以看出,生產者和消費者在回圈交替的執行任務,場面非常和諧,是我們想要的正確結果,
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();
}
}
程式執行結果如下:
從以上結果可以看出,當我們將生產者的數量增加到 2 個時,就會造成執行緒“假死”阻塞執行的問題,當生產者 2 被喚醒又被阻塞之后,整個程式就不能繼續執行了,
執行緒“假死”問題分析
我們先把以上程式的執行步驟標注一下,得到如下結果:
從上圖可以看出:
當執行到第 ④ 步時,此時生產者為作業狀態,而生產者 2 和消費者為等待狀態
此時正確的做法應該是喚醒消費者進行消費,然后消費者消費完之后再喚醒生產者繼續作業;
但此時生產者卻錯誤的喚醒了生產者 2,而生產者 2 因為佇列已經滿了,所以自身并不具備繼續執行的能力,因此就導致了整個程式的阻塞,流程圖如下所示:
正確執行流程應該是這樣的:
1.3 使用 Condition
為了解決執行緒的“假死”問題,我們可以使用 Condition 來嘗試實作一下,Condition 是 JUC(java.util.concurrent)包下的類,需要使用 Lock 鎖來創建,Condition 提供了 3 個重要的方法:
- await:對應 wait 方法;
- signal:對應 notify 方法;
- signalAll: notifyAll 方法,
因為 Condition 可以創建多個等待集,以本文的生產者和消費者模型為例,我們可以使用兩個等待集,一個用作消費者的等待和喚醒,另一個用來喚醒生產者,這樣就不會出現生產者喚醒生產者的情況了(生產者只能喚醒消費者,消費者只能喚醒生產者)這樣整個流程就不會“假死”了,它的執行流程如下圖所示:
了解了它的基本流程之后,咱們來看具體的實作代碼,
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();
}
}
程式的執行結果如下圖所示:
這個效果怎么樣,按部就班,誰也不干擾誰,一點點得執行,是不是很好,但是,再美好得背后,肯定有更覺大的危機,不信?接著往下看
2.性能問題
在上面我們演示 notify 會造成執行緒的“假死”問題的時候,那有的朋友可能會說:如果把 notify 換成 notifyAll 執行緒就不會“假死”了,豈不是更簡單?
我不多說,直接代碼執行大家看結果
工廠類我還是使用之前的Fctory代碼,只不過把notify更改為notifyAll()方法
依舊是兩個生產者加一個消費者
執行的結果如下圖所示:
通過以上結果可以看出:當我們呼叫 notifyAll 時確實不會造成執行緒“假死”了,但會造成所有的生產者都被喚醒了,但因為待執行的任務只有一個,因此被喚醒的所有生產者中,只有一個會執行正確的作業,而另一個則是啥也不干,然后又進入等待狀態,這種行為對于整個程式來說,無疑是多此一舉,只會增加執行緒調度的開銷,從而導致整個程式的性能下降,
反觀 Condition 的 await 和 signal 方法,即使有多個生產者,程式也只會喚醒一個有效的生產者進行作業,如下圖所示:
生產者和生產者 2 依次會被交替的喚醒進行作業,所以這樣執行時并沒有任何多余的開銷,從而相比于 notifyAll 而且整個程式的性能會提升不少,
總結
本文我們通過代碼和流程圖的方式演示了 wait 方法和 notify/notifyAll 方法的使用缺陷,它的缺陷主要有兩個,一個是在極端環境下使用 notify 會造成程式“假死”的情況,另一個就是使用 notifyAll 會造成性能下降的問題,因此在進行執行緒通訊時,強烈建議使用 Condition 類來實作,
PS:有人可能會問為什么不用 Condition 的 signalAll 和 notifyAll 進行性能對比?而使用 signal 和 notifyAll 進行對比?我只想說,既然使用 signal 可以實作此功能,為什么還要使用 signalAll 呢?這就好比在有暖氣的 25 度的房間里,穿一件短袖就可以了,為什么還要穿一件棉襖呢?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/236900.html
標籤:Java
