全文使用synchronized來說明。
synchronized給對象上鎖,先上偏向鎖,在上輕量級鎖,最後上重量級鎖。上什麼鎖,是gvm根據競争程度自行變換的。
重量級鎖
計算機作業系統本有Monitor對象,稱為管程。在java裡面看不到此對象。
每個Java對象都可以關聯一個monitor對象,如果使用了synchronized給對象上重量級鎖後,該對象的Mark word就被設定指向monitor對象的指針。
Monitor對象結構
- WaitSet:是線程等待隊列。狀态為WAITING
- EntryList:線程阻塞隊列.狀态為BLOCKED
- Owner:正在執行的線程(可能很多線程競争一個資源,但隻有一個線程能夠成功,此時Owner就置為此線程)
原理
-
建立的對象,此時Mark Word關聯一個Monitor對象(即Mark Word記錄一個monitor對象的位址)。此時Monitor對象裡面的Owner為null.
因為還沒有線程去獲得Monitor鎖。
-
多個線程競争,隻有線程1成功。其他進入阻塞隊列。(或者說,隻要owner非空,那麼其他線程就要進入阻塞隊列)
當線程1執行完畢後,通知阻塞隊列裡的線程,引起它們的非公平性競争。
-
若此時Owner線程調用wait方法,那麼會進入WaitSet。
當被喚醒時(如調用notify())會進入EntryList重新競争。
輕量級鎖
如果一個對象雖然有多線程通路,但多線程通路的時間是錯開的(或者說沒有競争)的話,可以使用輕量級鎖來優化。
當有競争時,會發生鎖膨脹,變為重量級鎖,
java 對象頭
以32為虛拟機為例
普通對象是:
- Klass Word:是一個指針,通過他可以知道是個啥類對象
- Mark Word:
如 Normal ,即沒有上鎖,末尾兩位是01;輕量級鎖是00
下面用Hashcode age Bias 01 代替最初的Mark Word
原理
每個線程的棧幀都會包含一個鎖記錄的結構,内部可以儲存鎖對象的Mark Word
- 當線程執行到臨界區代碼時,對obj上鎖。讓鎖記錄中的Object reference 指向鎖對象,并嘗試用cas替換Object的Mark Word,将 Mark Word的值存入鎖記錄。
- cas替換成功:那麼Object對象就會存儲鎖記錄狀态00和位址 ,表示由該線程給對象加鎖。 當Mark Word末尾是01時,才可以替換成功
- cas替換失敗
- case 1:其他線程已經持有了改Object的輕量級鎖,表明有競争,進入鎖膨脹過程
- case 2:自己執行synchronized鎖重入,再添加一條Lock Record作為重入的計數
此時進行cas操作自然會失敗。
最後再講鎖膨脹。
- 解鎖
- 當Lock Record 記錄裡面存在null,說明存在重進入,這時重置鎖記錄,表示重進入計數減一
- 鎖記錄不為空,這時用cas操作把鎖記錄裡面儲存的Mark Word替換給對象頭
- 成功則解鎖成功
- 失敗說明進行了鎖膨脹或已經更新為重量級鎖,進入重量級鎖的解鎖流程。
鎖膨脹
在嘗試加輕量級鎖的過程中,CAS操作無法成功,一個原因就是有其他線程為此對象加上了輕量級鎖,這時需要進行鎖膨脹,将輕量級鎖變為重量級鎖。
如線程1持有object的鎖,這時線程2也想要,但進行cas操作失敗,這時候會發生:
- 為object對象申請Monitor鎖,讓object指向重量級鎖位址(此時後兩位為10,即重量級鎖)
- 然後線程2進入Monitor的EntryList阻塞
當線程1執行完後,想要退出臨界區,使用cas将Mark Word的值替換給對象頭,但是會失敗。因為object的Mark Word後兩位變為10,已經不再是00了。
這時按照Monitor位址找到monitor對象,設定Owner為null,再喚醒線程2.
偏向鎖
如果隻有一個線程,它多次重進入,那會多次建立空的鎖記錄,作為鎖重入的計數。
是以,java 6中引入了偏向鎖來做進一步優化:隻有第一次使用CAS将線程ID設定到對象的Mark Word,之後隻要發現這個線程ID是自己的就表示沒有發生競争,不用重新CAS。以後隻要不發生競争,這個對象就歸該線程所有。
當線程請求到鎖對象後,将鎖對象的狀态标志位改為01,即偏向模式。然後使用CAS操作将線程的ID記錄在鎖對象的Mark Word中。以後該線程可以直接進入同步塊,連CAS操作都不需要。但是,一旦有第二條線程需要競争鎖,那麼偏向模式立即結束,進入輕量級鎖的狀态。
參考:https://www.bilibili.com/video/BV16J411h7Rd?from=search&seid=659615821890387531