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()來實現的。