執行緒的虛假喚醒情況
阿新 • • 發佈:2020-10-04
多執行緒併發操作一直都是學習和工作過程中的難點,一般而言,在多個執行緒共享資源時,我們通常會使用synchronized程式碼塊的同步,並通過wait()、notify()和notifyAll()來喚醒或者等待執行緒(這三個方法必須使用在同步程式碼塊或同步方法中,被同步監視器呼叫,否則會丟擲異常)。
還是通過經典的生產者和消費者案例引出虛假喚醒的問題
public class SpuriousWakeupDemo { public static void main(String[] args) { ShareData resources = new ShareData(); new Thread(() -> { for (int i = 0; i < 5; i++) { try { resources.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "producer").start(); new Thread(() -> { for (int i = 0; i < 5; i++) { try { resources.decrement(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }, "consumer").start(); } } /** * 資源類 */ class ShareData{ private int number = 0;//初始值為零的一個變數 public synchronized void increment() throws InterruptedException { //1判斷 if (number != 0) { this.wait(); } //2幹活 ++number; System.out.println(Thread.currentThread().getName() + "\t" + number); //3通知 this.notifyAll(); } public synchronized void decrement() throws InterruptedException { // 1判斷 if (number == 0) { this.wait(); } // 2幹活 --number; System.out.println(Thread.currentThread().getName() + "\t" + number); // 3通知 this.notifyAll(); } }
多次測試結果如下:
在main方法中通過匿名內部類的方式建立了兩個執行緒,一個作為生產者,一個作為消費者。上面這個程式碼正常執行,也並沒有出現什麼安全問題。但是我們卻忽略了一個重要的點!!過我們們改動程式碼,在main方法中多加如兩個生產者和消費者如下:
public class SpuriousWakeupDemo { public static void main(String[] args) { ShareData resources = new ShareData(); new Thread(() -> { for (int i = 0; i < 5; i++) { try { resources.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "producer1").start(); new Thread(() -> { for (int i = 0; i < 5; i++) { try { resources.decrement(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }, "consumer1").start(); new Thread(() -> { for (int i = 0; i < 5; i++) { try { resources.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "producer2").start(); new Thread(() -> { for (int i = 0; i < 5; i++) { try { resources.decrement(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }, "consumer2").start(); } }
多次測試,出現了以下結果:
我們發現ShareData類中的共享變數number即使在不等於0的情況下依然在自增,increment()方法明明已經通過了synchronized進行了加鎖,並且在方法內部做了判斷當number!=0就將執行緒等待,為什麼還是會出現number>0的情況?
解決上述問題很簡單,就是將if判斷改為while判斷,上述問題就再也沒有出現。
class ShareData{ private int number = 0;//初始值為零的一個變數 public synchronized void increment() throws InterruptedException { //1判斷 while (number != 0) { this.wait(); } //2幹活 ++number; System.out.println(Thread.currentThread().getName() + "\t" + number); //3通知 this.notifyAll(); } public synchronized void decrement() throws InterruptedException { // 1判斷 while (number == 0) { this.wait(); } // 2幹活 --number; System.out.println(Thread.currentThread().getName() + "\t" + number); // 3通知 this.notifyAll(); } }
這個問題的關鍵就在於當執行緒被喚醒時,從哪裡執行。當我們使用if作為條件判斷時,分別在wait()方法前後加兩條列印語句
class ShareData{
private int number = 0;//初始值為零的一個變數
public synchronized void increment() throws InterruptedException {
//1判斷
if (number != 0) {
System.out.println("生產執行緒"+Thread.currentThread().getName()+"準備休眠");
this.wait();
System.out.println("生產執行緒"+Thread.currentThread().getName()+"休眠結束");
}
//2幹活
++number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
//3通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 1判斷
if (number == 0) {
this.wait();
}
// 2幹活
--number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3通知
this.notifyAll();
}
}
控制檯列印結果如下:
將if改為while條件後述情況就不會存在,由於是while()迴圈,所有被等待的執行緒在獲取到cpu執行權利後一定會進行條件判斷,不會出現兩個生產者交替獲得CPU執行權將number+1的情況(也不會出現兩個消費者交替獲得cpu執行權的情況)
如圖: