1. 程式人生 > 實用技巧 >執行緒的虛假喚醒情況

執行緒的虛假喚醒情況

​ 多執行緒併發操作一直都是學習和工作過程中的難點,一般而言,在多個執行緒共享資源時,我們通常會使用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執行權的情況)

如圖: