關(guān)于Monitor對象在sychronized實(shí)現(xiàn)中的應(yīng)用(轉(zhuǎn))

JVM規(guī)范規(guī)定JVM基于進(jìn)入和退出Monitor對象來實(shí)現(xiàn)方法同步和代碼塊同步,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣。代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn),而方法同步是使用另外一種方式實(shí)現(xiàn)的,細(xì)節(jié)在JVM規(guī)范里并沒有詳細(xì)說明,但是方法的同步同樣可以使用這兩個(gè)指令來實(shí)現(xiàn)。monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結(jié)束處和異常處, JVM要保證每個(gè)monitorenter必須有對應(yīng)的monitorexit與之配對。任何對象都有一個(gè) monitor 與之關(guān)聯(lián),當(dāng)且一個(gè)monitor 被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到 monitorenter 指令時(shí),將會(huì)嘗試獲取對象所對應(yīng)的 monitor 的所有權(quán),即嘗試獲得對象的鎖。

4.1 Java對象頭

鎖存在Java對象頭里。如果對象是數(shù)組類型,則虛擬機(jī)用3個(gè)Word(字寬)存儲對象頭,如果對象是非數(shù)組類型,則用2字寬存儲對象頭。在32位虛擬機(jī)中,一字寬等于四字節(jié),即32bit。


長度

內(nèi)容

說明

32/64bit

Mark Word

存儲對象的hashCode或鎖信息等。

32/64bit

Class Metadata Address

存儲到對象類型數(shù)據(jù)的指針

32/64bit

Array length

數(shù)組的長度(如果當(dāng)前對象是數(shù)組)


Java對象頭里的Mark Word里默認(rèn)存儲對象的HashCode,分代年齡和鎖標(biāo)記位。32位JVM的Mark Word的默認(rèn)存儲結(jié)構(gòu)如下:


25 bit

4bit

1bit

是否是偏向鎖

2bit

鎖標(biāo)志位

無鎖狀態(tài)

對象的hashCode

對象分代年齡

0

01

在運(yùn)行期間Mark Word里存儲的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。Mark Word可能變化為存儲以下4種數(shù)據(jù):

鎖狀態(tài)

25 bit

4bit

1bit

2bit

23bit

2bit

是否是偏向鎖

鎖標(biāo)志位

輕量級鎖

指向棧中鎖記錄的指針

00

重量級鎖

指向互斥量(重量級鎖)的指針

10

GC標(biāo)記

11

偏向鎖

線程ID

Epoch

對象分代年齡

1

01

在64位虛擬機(jī)下,Mark Word是64bit大小的,其存儲結(jié)構(gòu)如下:?


鎖狀態(tài)

25bit

31bit

1bit

4bit

1bit

2bit



cms_free

分代年齡

偏向鎖

鎖標(biāo)志位

無鎖

unused

hashCode



0

01

偏向鎖

ThreadID(54bit) Epoch(2bit)



1

01


4.2 鎖的升級

Java SE1.6為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6里鎖一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài),輕量級鎖狀態(tài)和重量級鎖狀態(tài),它會(huì)隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率,下文會(huì)詳細(xì)分析。

4.3 偏向鎖

Hotspot的作者經(jīng)過以往的研究發(fā)現(xiàn)大多數(shù)情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。當(dāng)一個(gè)線程訪問同步塊并獲取鎖時(shí),會(huì)在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時(shí)不需要花費(fèi)CAS操作來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖,如果測試成功,表示線程已經(jīng)獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖),如果沒有設(shè)置,則使用CAS競爭鎖,如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程。

偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競爭偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖。偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行),它會(huì)首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動(dòng)狀態(tài),則將對象頭設(shè)置成無鎖狀態(tài),如果線程仍然活著,擁有偏向鎖的棧會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無鎖或者標(biāo)記對象不適合作為偏向鎖,最后喚醒暫停的線程。下圖中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。

關(guān)閉偏向鎖:偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲-XX:BiasedLockingStartupDelay = 0。如果你確定自己應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖-XX:-UseBiasedLocking=false,那么默認(rèn)會(huì)進(jìn)入輕量級鎖狀態(tài)。

4.4 輕量級鎖

輕量級鎖加鎖:線程在執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。

輕量級鎖解鎖:輕量級解鎖時(shí),會(huì)使用原子的CAS操作來將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當(dāng)前鎖存在競爭,鎖就會(huì)膨脹成重量級鎖。下圖是兩個(gè)線程同時(shí)爭奪鎖,導(dǎo)致鎖膨脹的流程圖。

因?yàn)樽孕龝?huì)消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會(huì)再恢復(fù)到輕量級鎖狀態(tài)。當(dāng)鎖處于這個(gè)狀態(tài)下,其他線程試圖獲取鎖時(shí),都會(huì)被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會(huì)喚醒這些線程,被喚醒的線程就會(huì)進(jìn)行新一輪的奪鎖之爭。

對于上面的描述一直有幾點(diǎn)不理解的:

1.第一段提到shronized實(shí)現(xiàn)利用的是Monitor對象,但是所有的對象都有Mark-word,所以sychronized利用的是Monior對象中的還是寄生對象(也就是與Monitor關(guān)聯(lián)的對象)的?

2.后面提到的偏向鎖,輕量級鎖和重量級鎖指定的鎖對象都是Monior嗎?

首先對于第一點(diǎn),其實(shí)是由于沒有搞清楚其實(shí)Monitor是一個(gè)輔助的鎖對象,實(shí)際上是一個(gè)互斥量對象,也就是shcronized作為重量級鎖使用的對象。對于第二點(diǎn),是由于第一段起到了一定誤導(dǎo)作用,JVM對于鎖的優(yōu)化才會(huì)出現(xiàn)偏向鎖,輕量級鎖和重量級鎖三種情況,所以說shronized實(shí)現(xiàn)利用的是Monitor對象我感覺是很不妥的,因?yàn)橹挥性阪i膨脹到重量級鎖的時(shí)候?qū)ο箢^才會(huì)指向Monitor對象。

所以我更傾向于下面的說法:

synchronized的底層實(shí)現(xiàn)就用到了臨界區(qū)和互斥鎖(重量級鎖的情況下)這兩個(gè)概念。

出自一下博客:https://www.cnblogs.com/dennyzhangdd/p/6734638.html#_label2。

下面依照個(gè)人理解來描述下sychronized加鎖和鎖膨脹的流程:

1:由于java6之后偏向鎖是開啟的,也就是說當(dāng)對一個(gè)代碼塊或者方法加上sychronized的時(shí)候先進(jìn)入的是偏向鎖獲取狀態(tài)。這時(shí)候依據(jù)4.1的步驟,通過CAS操作設(shè)置Mark-word格式如下

偏向鎖

線程ID

Epoch

對象分代年齡

1

01


可見,偏向鎖獲取的階段,只是指向了獲取偏向鎖的線程id,并沒有用到Monitor對象。

2.當(dāng)獲取到某一個(gè)線程獲取偏向鎖的時(shí)候發(fā)現(xiàn)已經(jīng)有線程獲取了該對象的鎖,當(dāng)達(dá)到全局安全點(diǎn)(safepoint),獲得偏向鎖的線程被掛起,撤銷偏向鎖,恢復(fù)到無鎖(標(biāo)志位為?01)或輕量級鎖(標(biāo)志位為?00)的狀態(tài);這取決于持有偏向鎖的線程是否還處于活動(dòng)狀態(tài),如果不處于活動(dòng)狀態(tài),則轉(zhuǎn)為無鎖狀態(tài),如仍然活著這升級為輕量級鎖,將Mark-word中的鎖記錄指向當(dāng)前棧中的鎖記錄。

3.在輕量級鎖獲取階段,Mark-word的格式如下,這時(shí)候Mark-word指向的對象也不是Monitor對象,而是線程棧幀中開辟的一塊用于存儲鎖記錄的空間,存儲的內(nèi)容為Mark-word中的內(nèi)容(個(gè)人認(rèn)為是無鎖狀態(tài)下對象的hashcode,分代年齡等信息),稱為Displaced Mark-word。

輕量級鎖

指向棧中鎖記錄的指針

00


4.在輕量級鎖獲取階段,如果同時(shí)又其他線程競爭鎖,這線程通過自旋來獲取鎖(循環(huán)CAS來修改Mark-word指向自己的鎖記錄地址),當(dāng)自旋超時(shí)或者超過一定次數(shù)后,獲取鎖失敗,導(dǎo)致鎖膨脹為重量級鎖,就是將Mark-word中的鎖標(biāo)志置為10,并指向Monitor對象,然后阻塞當(dāng)前線程,當(dāng)鎖膨脹完成并返回對應(yīng)的monitor時(shí),并不表示該線程競爭到了鎖。當(dāng)持有輕量級鎖的線程再次嘗試進(jìn)行CAS時(shí),這時(shí)候已經(jīng)被競爭線程修改,所以失敗,釋放鎖,喚醒阻塞的線程,進(jìn)入重量級鎖階段。

5.openjdk\hotspot\src\share\vm\runtime\objectMonitor.hpp源碼如下:

1 ObjectMonitor() {

2? ? _header? ? ? = NULL;//markOop對象頭

3? ? _count? ? ? ? = 0;

4? ? _waiters? ? ? = 0,//等待線程數(shù)

5? ? _recursions? = 0;//重入次數(shù)

6? ? _object? ? ? = NULL;//監(jiān)視器鎖寄生的對象。鎖不是平白出現(xiàn)的,而是寄托存儲于對象中。

7? ? _owner? ? ? ? = NULL;//指向獲得ObjectMonitor對象的線程或基礎(chǔ)鎖

8? ? _WaitSet? ? ? = NULL;//處于wait狀態(tài)的線程,會(huì)被加入到wait set;

9? ? _WaitSetLock? = 0 ;

10? ? _Responsible? = NULL ;

11? ? _succ? ? ? ? = NULL ;

12? ? _cxq? ? ? ? ? = NULL ;

13? ? FreeNext? ? ? = NULL ;

14? ? _EntryList? ? = NULL ;//處于等待鎖block狀態(tài)的線程,會(huì)被加入到entry set;

15? ? _SpinFreq? ? = 0 ;

16? ? _SpinClock? ? = 0 ;

17? ? OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock

18? ? _previous_owner_tid = 0;// 監(jiān)視器前一個(gè)擁有者線程的ID

19? }

以下引用自:https://blog.csdn.net/zqz_zqz/article/details/70233767

Monitor對象的結(jié)構(gòu)如下:

Contention List:競爭隊(duì)列,所有請求鎖的線程首先被放在這個(gè)競爭隊(duì)列中;

Entry List:Contention List中那些有資格成為候選資源的線程被移動(dòng)到Entry List中;

Wait Set:哪些調(diào)用wait方法被阻塞的線程被放置在這里;

OnDeck:任意時(shí)刻,最多只有一個(gè)線程正在競爭鎖資源,該線程被成為OnDeck;

Owner:當(dāng)前已經(jīng)獲取到所資源的線程被稱為Owner;

!Owner:當(dāng)前釋放鎖的線程。

JVM每次從隊(duì)列的尾部取出一個(gè)數(shù)據(jù)用于鎖競爭候選者(OnDeck),但是并發(fā)情況下,ContentionList會(huì)被大量的并發(fā)線程進(jìn)行CAS訪問,為了降低對尾部元素的競爭,JVM會(huì)將一部分線程移動(dòng)到EntryList中作為候選競爭線程。Owner線程會(huì)在unlock時(shí),將ContentionList中的部分線程遷移到EntryList中,并指定EntryList中的某個(gè)線程為OnDeck線程(一般是最先進(jìn)去的那個(gè)線程)。Owner線程并不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權(quán)利交給OnDeck,OnDeck需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大的提升系統(tǒng)的吞吐量,在JVM中,也把這種選擇行為稱之為“競爭切換”。

OnDeck線程獲取到鎖資源后會(huì)變?yōu)镺wner線程,而沒有得到鎖資源的仍然停留在EntryList中。如果Owner線程被wait方法阻塞,則轉(zhuǎn)移到WaitSet隊(duì)列中,直到某個(gè)時(shí)刻通過notify或者notifyAll喚醒,會(huì)重新進(jìn)去EntryList中。

處于ContentionList、EntryList、WaitSet中的線程都處于阻塞狀態(tài),該阻塞是由操作系統(tǒng)來完成的(Linux內(nèi)核下采用pthread_mutex_lock內(nèi)核函數(shù)實(shí)現(xiàn)的)。

Synchronized是非公平鎖。?Synchronized在線程進(jìn)入ContentionList時(shí),等待的線程會(huì)先嘗試自旋獲取鎖,如果獲取不到就進(jìn)入ContentionList,這明顯對于已經(jīng)進(jìn)入隊(duì)列的線程是不公平的,還有一個(gè)不公平的事情就是自旋獲取鎖的線程還可能直接搶占OnDeck線程的鎖資源。

綜上:

JVM對Synchronized的優(yōu)化,簡單來說解決三種場景:

1)只有一個(gè)線程進(jìn)入臨界區(qū),偏向鎖

2)多個(gè)線程交替進(jìn)入臨界區(qū),輕量級鎖

3)多線程同時(shí)進(jìn)入臨界區(qū),重量級鎖

所以說Monitor對象只是實(shí)現(xiàn)sychronized對象的其中一部分(重量級鎖部分)。

https://blog.csdn.net/super_x_man/article/details/81741073

https://baijiahao.baidu.com/s?id=1639857097437674576&wfr=spider&for=pc

http://www.itdecent.cn/p/435c20a64da1

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容