深入理解wait()、notify()和notifyAll()方法爲什麼屬於Object,爲什麼要在synchronized代碼塊中

        LZ在網上看了很多關於這個問題的解釋,都不夠深入,那麼今天就讓我帶大家深入瞭解這個問題。關於synchronized的詳細介紹請移步大神所寫的博客:深入理解Java併發之synchronized實現原理,這篇文檔稍微有點長,我會用自己的話總結一下關於wait()、notify()和notifyAll()的問題。

        說到Object的這三個方法,就需要先說一下synchronized關鍵字,我們知道每一個實例對象或是類對象都可以作爲一把鎖來使用,例如:

public class SynchronizedTest{
    private static int i = 0;

    public static synchronized void increase(){  //加鎖的對象是SynchronizedTest的類對象
        i++;
    }

    public synchronized void increase4Obj(){  //加鎖的對象當時調用該方法的實例對象
        i++;
    }

    public void increase2Obj(){
        synchronized (Object.class){   //加鎖的對象是Object的類對象
            i++;
        }
    }
}

       上面方式是synchronized實現鎖的三種方式,這一點不懂的還請看一下我上面提到的大神寫的博客,之所以每個對象都可以當成一把鎖把使用,是因爲每一個對象都有唯一的一個monitor對象與之關聯,JVM中對象的佈局如下(直接複製大神博客裏面的圖片)

                         

 

  • 實例變量:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分內存按4字節對齊。
  • 填充數據:由於虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊,這點了解即可。

而對於頂部,則是Java頭對象,它實現synchronized的鎖對象的基礎,這點我們重點分析它,一般而言,synchronized使用的鎖對象是存儲在Java對象頭裏的,jvm中採用2個字來存儲對象頭(如果對象是數組則會分配3個字,多出來的1個字記錄的是數組長度),其主要結構是由Mark Word 和 Class Metadata Address 組成,其結構說明如下表:

虛擬機位數 頭對象結構 說明
32/64bit Mark Word 存儲對象的hashCode、鎖信息或分代年齡或GC標誌等信息
32/64bit Class Metadata Address 類型指針指向對象的類元數據,JVM通過這個指針確定該對象是哪個類的實例。

       其中在Mark Word中的信息會根據鎖的狀態進行動態的變換,當鎖的狀態爲重量級鎖的時候,Mark Word中會有一個引用指向monitor對象(每一個對象都會對應一個monitor對象),monitor對象的結構如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄重入的次數個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

       其中比較關鍵的變量有

           _count:記錄當前持有monitor對象的線程重入的次數

           _owner:記錄當前持有monitor對象的線程

           _WaitSet:等待集合,當前持有monitor對象的線程,調用wait()方法會釋放monitor對象鎖,然後進入該集合等待被喚醒

           _EntryList:等待獲取鎖的集合,在進入synchronized方法或synchronized修飾的代碼塊時,競爭獲取monitor對象鎖失敗的線程會進入該集合等待機會競爭monitor對象鎖。

       其中的關係如下圖所示:

                

    知道了synchronized的原理後,就可以回答上面兩個問題了:

           一:wait()、notify()和notifyAll()方法爲什麼要在synchronized代碼塊中?

           在Object的wait()方法上面有這樣一行註釋:The current thread must own this object's monitor,意思是調用實例對象的wait()方法時,該線程必須擁有當前對象的monitor對象鎖,而要擁有monitor對象鎖就需要在synchronized修飾的方法或代碼塊中競爭並生成佔用monitor對象鎖。而不使用synchronized修飾的方法或代碼塊就不會佔有monitor對象鎖,所以在synchronized代碼塊之外調用會出現錯誤,錯誤提示爲:

Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at it.cast.basic.thread.SynchronizedTest.main(SynchronizedTest.java:27)

           因爲只有擁有了monitor對象的線程才能調用wait()方法進入_WaitSet 等待集合中等待被喚醒,而且對於monitor對象來說,同一時刻只能被一個線程鎖持有,在不加synchronized修飾的時候(也就是不是有鎖的時候),monitor對象就不會被使用到,這裏突然使用wait()方法就是出現邏輯錯誤,所以必須在synchronized代碼塊中。

        二:wait()、notify()和notifyAll()方法爲什麼屬於Object?

           因爲每一個對象都可以被當作一把鎖來使用,對象在JVM中的內存劃分爲對象頭和實例變量,其中對象頭裏面有包含了對象的hashcode、鎖信息和分代年齡等信息,對象頭會根據鎖狀態的不同,動態的變換,當鎖升級爲重量級鎖的時候,對象頭會擁有一個monitor對象的引用,monitor對象也就是每一個對象實現鎖的關鍵,當所有的線程都競爭monitor對象鎖,只有一個線程能成功佔有鎖,其他線程都會進入阻塞進入等待集合中,成功佔有鎖的線程會接着執行代碼,在執行代碼的過程中,如果遇到wait()方法,當前線程會釋放掉monitor對象鎖,然後進入monitor對象的等待被喚醒的集合,喚醒的動作是通過notify()和notifyAll()來實現的。

發佈了221 篇原創文章 · 獲贊 30 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章