為什么object.wait()、object.notify()一定要放在synchronized代碼塊內?


    相信大多數人對object.wait()和object.notify()都非常熟悉,最經典的生產者-消費者模型就可以基於wait-notify機制來實現的,那么在編寫代碼的時候發現,JDK要求對object.wait()和object().notify方法必須在synchronized代碼塊內部使用,否則運行時會拋出IllegalMonitorStateException異常。那么為什么JDK要對此做限制呢?

    要想知道為什么要加此限制,就得知道不加此限制會發生什么非預期的問題。如果不加這個限制,一個簡單的生產者-消費者模型的實現如下:當count為0的時候,生產者進行生產操作,並將count+1,然后調用notify()方法通知;當count為0時,消費者會調用wait()方法進行等待。(至於為什么消費者中要用while()方法,我們在后文介紹)

private int count = 0;
private Object obj;

public void producer(){
    if (count == 0){
        //省略生產者邏輯
        count++;
        obj.notify();
  }
}

public void consumer(){
    while (count == 0){
    obj.wait();    
    }
    //省略消費邏輯
}

    乍一看,通過上述代碼實現了生產者-消費者的功能,但是仔細一想,存在問題。假如此時線程T1在執行producer的邏輯,線程T2在執行consumer的邏輯,如果代碼的執行順序變成下面這樣,就會有問題:

        1.線程T2執行while (count == 0),表達式成立,進入while循環;

        2.線程T1執行if (count == 0),表達式成立,進入if消息體;

        3.線程T1執行if消息體內容,最終調用obj.notify()(注意,此時線程T2未執行obj.wait(),notify()不會喚醒任何線程);

        4.線程T2執行obj.wait()進行等待;

    這樣執行完之后,count的值為1,生產者不會再進行生產操作(也就不會調用obj.notify(),而此時消費者線程T2處於等待狀態(需要obj.notify()來喚醒),消費者線程就永遠地死等下去了,這就是多線程編程中臭名昭著的Lost Wake-Up Problem問題。

為什么使用synchronized同步代碼塊能解決這個問題呢?

    仔細分析上面的問題,原因很簡單,就是對count變量的讀寫存在競態條件,舉個例子,consumer()方法原本的用意是在執行obj.wait()的時候,count的值必須為0,也就是obj.wait()和count為0是綁定的,但是此時如果有另外一個線程在執行producer()方法,可能就會在執行while (count == 0)到obj.wait()之間對count的值進行修改,從而出現非預期的情況(即在執行obj.wait()方法的時候,count的值不是0)。

    而使用synchronized同步代碼塊后,代碼變成這個樣子:

private int count = 0;
private Object obj;
 
public void producer(){
    synchonized(obj){
        if (count == 0){
            //省略生產邏輯
            count++;
            obj.notify();
        }
    }
}
 
public void consumer(){
    synchonized(obj){
        while (count == 0){
            obj.wait();    
        }
        //省略消費邏輯
    }
}

 

  此時,由於只有線程拿到obj對象的鎖才能進入同步代碼塊,所以能夠保證生產者和消費者只有一個線程在執行,也就保證了在執行while (count == 0)到obj.wait()之間count的值不會發生改變,也就是上面1、4步驟之間,不可能會有2、3步驟了。

為什么要使用while(count == 0)而不是if(count ==0)?

    要想知道為什么使用while(count == 0),可以先看看使用if(count ==0)會有什么問題。還是基於上面的代碼實例,當前兩個線程的執行情況是,線程T1執行obj.notify()方法,線程T2執行obj.wait()方法。如果此時有一個線程T3也作為消費者開始執行consumer()方法,可能會出現這種情況:

    1.線程T1執行obj.notify();

    2.線程T2被喚醒(注意:喚醒操作只是將線程從管程中的等待隊列中拿取來放到管程的入口隊列中去競爭鎖,而不是直接得到鎖),嘗試去競爭obj對象鎖;

    3.線程T3執行consumer()方法,競爭到鎖,並進行消費,將count-1,即count又變為0,然后釋放鎖;

    4.線程T2獲取到鎖,執行消費邏輯(因為為if(count == 0),所以直接往下執行消費邏輯了);

很明顯,此時count已經被線程T3消費掉了,count的值又變回0了,線程T2去執行消費邏輯是存在問題的,這就是虛假喚醒的問題。但是如果將if(count == 0)改為while(count == 0)就不會有問題了,因為線程T2拿到鎖之后還會去判斷一下count的值是不是0,非0的情況下才會去執行消費的邏輯。

    所以,使用等待-通知機制時有一個經典范式:

while(條件不滿足){
    wait();
}

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM