JAVA線程虛假喚醒 線程虛假喚醒問題描述 在JDK API文檔中,關於Object類的wait()方法有這樣一句話描述"線程也可以喚醒,而不會被通知,中斷或超時,即所謂的虛假喚醒 。 雖然這在實踐中很少會發生,但應用程式必須通過測試應該使線程被喚醒的條件來防範,並且如果條件不滿足則繼續等待", ...
線程虛假喚醒問題描述
在JDK API文檔中,關於Object類的wait()方法有這樣一句話描述"線程也可以喚醒,而不會被通知,中斷或超時,即所謂的虛假喚醒 。 雖然這在實踐中很少會發生,但應用程式必須通過測試應該使線程被喚醒的條件來防範,並且如果條件不滿足則繼續等待",如下圖所示:
在多線程的情況下,當多個線程執行了wait()方法後,需要其它線程執行notify()或者notifyAll()方法去喚醒,假如被阻塞的多個線程都被喚醒,但實際情況是被喚醒的線程中有一部分線程是不應該被喚醒的,那麼對於這些不應該被喚醒的線程而言就是虛假喚醒。
問題復現
生產者與消費者問題
假設當前有4個線程分別為A,B,C,D,其中A,B線程是生產者,C,D線程是消費者,當A和B線程生產了一個數據後就去通知消費者去消費,C和D消費掉這一個數據後就通知生產者去生產,數據的大小為1。也就是說正常情況下,數據只會有0和1兩種狀態,0表示生產者該生產數據了,1表示消費者該消費數據了。
package producer_consumer;
public class PVTest {
public static void main(String[] args) {
Data data = new Data();
//生產者線程A
new Thread(() -> {
for (int i = 0;i < 5;i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//生產者線程B
new Thread(() -> {
for (int i = 0;i < 5;i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
//消費者線程C
new Thread(() -> {
for (int i = 0;i < 5;i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
//消費者線程D
new Thread(() -> {
for (int i = 0;i < 5;i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//數據類
class Data {
//表示數據個數
private int number = 0;
public synchronized void increment() throws InterruptedException {
//關鍵點,這裡應該使用while迴圈
if (number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"生產了數據:"+number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
//關鍵點,這裡應該使用while迴圈
if (number == 0) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"消費了數據:"+number);
this.notifyAll();
}
}
程式結果
結果分析
可以看到上述結果出現了data為2的情況,不符合之前的預期,出現問題的場景是這樣的:當data為1的時候,線程A和B先後獲取鎖去生產數據的時候會被阻塞住,然後消費者C或者D消費掉數據後去notifyAll()喚醒了線程A和B,被喚醒的A和B沒有再次去判斷data狀態,就去執行後續增加數據的邏輯了,導致兩個生產者都執行了increment(),最終data出現了2這種情況。也就是說線程A和B有一個是不應該被喚醒的卻被喚醒了,出現這個問題的關鍵點在於程式中使用到了if判斷,只判斷了一次data的狀態,應該使用while迴圈去判斷
虛假喚醒問題解決
正如JDK API文檔中所說在寫程式時候應該用while去替代if,上述生產者和消費者代碼中,將Data類中的increment()和decrement()方法中的if換為while即可避免線程虛假喚醒的問題。