上一篇通過構建金字塔結構,來從不同的角度,由淺入深的對synchronized關鍵字做了介紹,
快速跳轉:https://www.cnblogs.com/xyang/p/11631866.html
本文將從底層實現的各個“組件”着手,詳細拆解其工作原理。
本文會分為以下2節內容:
第一節:介紹MarkWord和LockRecord兩種數據結構,該知識點是理解synchronized關鍵字底層原理的關鍵。
第二節:分析偏向鎖加鎖解鎖時機和過程
一.先來了解兩種數據結構,你應該了解這些知識點
1.MarkWord:在鎖的使用過程中會對鎖對象作出相應的操作
在HotSpot虛擬機中,Java對象在內存中存儲的布局,分為三個部分:對象頭,實例數據,對齊填充。
本文重點關注對象頭。
對象頭又划分為2或3部分,具體包括:
- MarkWord(后文簡稱MW,后續詳細介紹)
- 類型指針:指向這個對象所屬的類的元數據(klass)的指針
- 最后這一部分比較特殊,只有在對象是Java數組時才會存在,記錄的是數組的長度。為什么要存在這個記錄呢?我們知道,在普通Java對象中,我們可以通過讀取對象所屬類的元數據,計算出對象的大小。而數組是無法做到的,於是借助這塊區域來記錄。
本文重點關注MW區域
MW是一塊固定大小內存區域,在32位虛擬機中是32個bit,對應的,64位虛擬機中是64個bit。本文以32位虛擬機為例分析。
我們從直觀上理解,所謂的頭信息,一般都是用來記載一些不易變的信息,例如在http請求頭中的各種頭信息。在對象頭中也是如此,例如hashcode。在JVM虛擬機中為了解決存儲空間開銷,對象頭的MW大小已經固定。那么,要存儲的信息有比較多,包括且不限於:鎖標志位、GC信息、鎖相關信息,總大小遠遠超出32bit,怎么辦呢?
共享存儲區域,在不同的時刻,根據需求存儲需要的信息。
請參考下圖:
鎖類型 |
25bit |
4bit |
1bit |
2bit |
|
---|---|---|---|---|---|
23bit |
2bit |
是否偏向鎖 |
鎖標志位 |
||
無鎖 |
對象hashcode |
分代年齡 |
0 |
01 |
|
偏向鎖 |
線程ID |
epoch |
分代年齡 |
1 |
01 |
輕量級鎖 |
指向棧中鎖記錄的指針 |
00 |
|||
重量級鎖 |
指向互斥量 |
10 |
|||
GC標記 |
空 |
11 |
說明:兩個標志位最多只能標識4個狀態,那么剩下一個怎么辦?共享。無鎖和偏向鎖共享01狀態,他們兩個的區分
2.LockRecord:
在當前線程的棧中申請LR(LockRecord簡稱,下同),主要包含兩部分,第一步部分可以用於存放MW的副本;第二部分obj,用於指向鎖對象。
上述兩者的關系用下圖表示:
二.偏向鎖怎么工作
在對象創建的時候,MW會有一個初始態,要么是無鎖態,要么是初始偏向鎖態(ThreadId、epoch值都為初始值0)。程序員的世界不存在二義性,最終總會選一個,選擇的依據是虛擬機的配置參數,在JDK1.6以后,默認是開啟的,如果要禁用掉:-XX:-UseBiasedLocking。
什么時候需要禁用呢?如果能確認程序在大多數情況下,都存在多線程競爭,那么就可以禁用掉偏向鎖。沒必要每次都走一遍偏向鎖->輕量級鎖->重量級鎖的完整升級流程。
1.先放一張圖,直觀的描述偏向鎖的加鎖、解鎖、撤銷基本流程
2.加鎖過程
步驟一:
- LR記錄賦值:在當前線程的棧中,申請一個LR,把obj指向鎖對象
步驟二:如圖中所示,線程T1,執行到同步代碼,嘗試加偏向鎖,【判斷三要素:標志位,ThreadId,epoch】:
- 先用標志位判斷是否支持偏向鎖。鎖對象的對象頭MW區域后3個bit位的值是101。特別需要注意:如果是001,是無鎖狀態,代表偏向鎖不可用,會走加輕量級鎖流程。
- 再用ThreadId值,決定加鎖還是重入還是競爭:
- 0值加鎖。如果ThreadId=0,代表無任何線程持有該對象的偏向鎖,可以執行加鎖操作,進入加鎖流程;
- 非0值,
- 且為當前線程id,重入。如果ThreadId!=0,就判斷其值是否是當前線程的ID,分兩種情況:如果是,直接鎖重入,不再重復加鎖。
- 不為當前線程id,加鎖或競爭。非0值,說明是其他線程(圖中T2)已獲得了同步鎖。對象所屬Class里也會維護一個epoch值,這里我們簡稱為cEpoch,比較兩者:
-
- epoch小於類的cEpoch值,加鎖。如果epoch<cEpoch,說明發生過批量重偏向,當前鎖對象已被“釋放”了。此時進行“重偏向”(里說的釋放並非真正意義的釋放,而是隱含着一層意思:當前線程已經執行完同步塊,且在某次重偏向操作中,也檢測到這一點,不再維護epoch的最新值,這樣新的線程認為此時該偏向鎖,可以加鎖,直接CAS修改ThreadId即可)
- epoch等於類的cEpoch值,競爭鎖。
標准的可加鎖狀態MW內容如下圖所示:
鎖類型 |
25bit |
|
4bit |
1bit |
2bit |
|
23bit |
2bit |
|
是否偏向鎖 |
鎖標志位 |
偏向鎖 |
ThreadId==0 |
epoch==n |
分代年齡 |
1 |
01 |
或者
鎖類型 |
25bit |
|
4bit |
1bit |
2bit |
|
23bit |
2bit |
|
是否偏向鎖 |
鎖標志位 |
偏向鎖 |
ThreadId!=0 |
epoch==n(小於cepoch) |
分代年齡 |
1 |
01
|
第二步:通過CAS原子操作,把T1的ThreadId寫入MW。執行結果有兩種情況:
- 寫入成功,獲得偏向鎖,進入同步代碼塊執行同步邏輯。
- 寫入失敗,表明在第一步判斷和CAS操作之間,有其他線程已獲得了鎖。走鎖競爭邏輯。
2.解鎖過程
當前線程執行完同步代碼塊后,進行解鎖,解鎖操作比較簡單,僅僅將棧中的最近一條LR中的obj賦值為null。這里需要注意,MW中的threadId並不會做修改。
3.鎖競爭處理流程
T1嘗試加鎖,
如果存在鎖競爭情況,持有鎖的線程T2並不會在發現競爭的第一時間就直接撤銷鎖,或者升級鎖,而是執行到安全點后再處理。
-
- 此時如果當前線程已執行完同步塊代碼且線程已不存活,將會撤銷鎖至無鎖狀態,然后進入鎖升級邏輯。
- 否則,將會走鎖升級流程,升級為輕量級鎖,且升級完后T2繼續持有輕量級鎖,繼續執行同步代碼。
ps:怎么判斷是否還在執行同步代碼呢?遍歷棧中的RL,如果都為null,代表鎖已全部釋放。
4.批量重偏向和批量撤銷
有這樣一種場景:如果我們預判競爭不多,大部分情況下是單一線程執行同步塊,開啟了偏向鎖。但是在實際使用環境中,出現了大量的競爭,這時候怎么辦呢?停機重新配置參數?恐怕不是最好的方案。如果是我們來設計這個這個Synchronized鎖,肯定也會做一些兜底策略。比如這樣來做,當某一事件發生了N次,那么就更改一下處理策略?
是的,基本思想差不多,只不過更完善,暫時留一個懸念,在下次揭曉。