Java多線程中的虛假喚醒和如何避免

先來看一個例子

一個賣面的麪館,有一個做面的廚師和一個吃麪的食客,需要保證,廚師做一碗麪,食客喫一碗麪,不能一次性多做幾碗面,更不能沒有面的時候吃麪;按照上述操作,進行十輪做面吃麪的操作。

用代碼說話

首先我們需要有一個資源類,裏面包含面的數量,做面操作,吃麪操作;
當面的數量爲0時,廚師才做面,做完面,需要喚醒等待的食客,否則廚師需要等待食客吃完麪才能做面;
當面的數量不爲0時,食客才能吃麪,吃完麪需要喚醒正在等待的廚師,否則食客需要等待廚師做完面才能吃麪;
然後在主類中,我們創建一個廚師線程進行10次做面,一個食客線程進行10次吃麪;
代碼如下:

package com.duoxiancheng.code;

/**
 * @user: code隨筆
 */

class Noodles{

    //面的數量
    private int num = 0;

    //做面方法
    public synchronized void makeNoodles() throws InterruptedException {
        //如果面的數量不爲0,則等待食客吃完麪再做面
        if(num != 0){
            this.wait();
        }

        num++;
        System.out.println(Thread.currentThread().getName()+"做好了一份面,當前有"+num+"份面");
        //面做好後,喚醒食客來喫
        this.notifyAll();
    }

    //吃麪方法
    public synchronized void eatNoodles() throws InterruptedException {
        //如果面的數量爲0,則等待廚師做完面再吃麪
        if(num == 0){
            this.wait();
        }

        num--;
        System.out.println(Thread.currentThread().getName()+"吃了一份面,當前有"+num+"份面");
        //喫完則喚醒廚師來做面
        this.notifyAll();
    }

}

public class Test {

    public static void main(String[] args) {

        Noodles noodles = new Noodles();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.makeNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"廚師A").start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.eatNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"食客甲").start();

    }

}

輸出如下:


可以見到是交替輸出的;

如果有兩個廚師,兩個食客,都進行10次循環呢?

Noodles類的代碼不用動,在主類中多創建兩個線程即可,主類代碼如下:

public class Test {

    public static void main(String[] args) {

        Noodles noodles = new Noodles();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.makeNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"廚師A").start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.makeNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"廚師B").start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.eatNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"食客甲").start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.eatNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"食客乙").start();

    }
}

此時輸出如下:

虛假喚醒

上面的問題就是"虛假喚醒"。
當我們只有一個廚師一個食客時,只能是廚師做面或者食客吃麪,並沒有其他情況;
但是當有兩個廚師,兩個食客時,就會出現下面的問題:

  1. 初始狀態


  2. 廚師A得到操作權,發現面的數量爲0,可以做面,面的份數+1,然後喚醒所有線程;
  3. 廚師B得到操作權,發現面的數量爲1,不可以做面,執行wait操作;


  4. 廚師A得到操作權,發現面的數量爲1,不可以做面,執行wait操作;


  5. 食客甲得到操作權,發現面的數量爲1,可以吃麪,吃完麪後面的數量-1,並喚醒所有線程;
  1. 此時廚師A得到操作權了,因爲是從剛纔阻塞的地方繼續運行,就不用再判斷面的數量是否爲0了,所以直接面的數量+1,並喚醒其他線程;
  1. 此時廚師B得到操作權了,因爲是從剛纔阻塞的地方繼續運行,就不用再判斷面的數量是否爲0了,所以直接面的數量+1,並喚醒其他線程;



    這便是虛假喚醒,還有其他的情況,讀者可以嘗試畫畫圖分析分析。

解決方法

出現虛假喚醒的原因是從阻塞態到就緒態再到運行態沒有進行判斷,我們只需要讓其每次得到操作權時都進行判斷就可以了;
所以將

if(num != 0){
    this.wait();
}

改爲

while(num != 0){
    this.wait();
}

if(num == 0){
    this.wait();
}

改爲

while(num == 0){
    this.wait();
}

即可。

微信搜索:code隨筆 歡迎關注樂於輸出Java,算法等乾貨的技術公衆號。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章