1. 程式人生 > >【從刷面試題到構建知識體系】Java底層-synchronized鎖-2偏向鎖篇

【從刷面試題到構建知識體系】Java底層-synchronized鎖-2偏向鎖篇

上一篇通過構建金字塔結構,來從不同的角度,由淺入深的對synchronized關鍵字做了介紹,

快速跳轉:https://www.cnblogs.com/xyang/p/11631866.html

本文將從底層實現的各個“元件”著手,詳細拆解其工作原理。

本文會分為以下4節內容:

  第一節:介紹MarkWord和LockRecord兩種資料結構,該知識點是理解synchronized關鍵字底層原理的關鍵。

  第二節:分析偏向鎖加鎖解鎖時機和過程

一.先來了解兩種資料結構,你應該瞭解這些知識點

1.MarkWord:在鎖的使用過程中會對鎖物件作出相應的操作

 在HotSpot虛擬機器中,Java物件在記憶體中儲存的佈局,分為三個部分:物件頭,例項資料,對齊填充。

本文重點關注物件頭。

物件頭又劃分為2或3部分,具體包括:

  1. MarkWord(後文簡稱MW,後續詳細介紹)
  2. 型別指標:指向這個物件所屬的類的元資料(klass)的指標
  3. 最後這一部分比較特殊,只有在物件是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.加鎖過程

 步驟一:

  1. LR記錄賦值:在當前執行緒的棧中,申請一個LR,把obj指向鎖物件

步驟二:如圖中所示,執行緒T1,執行到同步程式碼,嘗試加偏向鎖,首先會做【偏向鎖是否可用】的判斷:

  1. 鎖物件的物件頭MW區域後3個bit位的值是101。特別需要注意:如果是001,是無鎖狀態,代表偏向鎖不可用,會走加輕量級鎖流程。
  2. ThreadId值:
    1. 如果ThreadId=0,代表無任何執行緒持有該物件的偏向鎖,可以執行加鎖操作,進入加鎖流程;
    2. 如果ThreadId!=0,就判斷其值是否是當前執行緒的ID,分兩種情況:如果是,直接鎖重入,不再重複加鎖。如果否,說明是其他執行緒(圖中T2)已獲得了同步鎖,進入“第三步”鎖競爭流程。
  3. epoch值:物件所屬Class裡也會維護一個epoch值,這裡我們簡稱為cEpoch,對該值的判斷,可能會導致兩種操作:
    1. 如果epoch<cEpoch,且ThreadId!=0,說明發生過批量重偏向,當前鎖物件已被“釋放”了。此時進行“重偏向”(裡說的釋放並非真正意義的釋放,而是隱含著一層意思:當前執行緒已經執行完同步塊,且在某次重偏向操作中,也檢測到這一點,不再維護epoch的最新值,這樣新的執行緒認為此時該偏向鎖,可以加鎖,直接CAS修改ThreadId即可)。
    2. 如果ThreadId==0,因為偏向鎖沒有顯示的撤銷修改ThreadId過程,說明肯定是初始狀態,那麼epoch值也肯定是初始狀態0,此時直接進行加鎖操作。

    可加鎖狀態的MW內容如下圖所示:

    

鎖型別

25bit

 

4bit

1bit

2bit

 

23bit

2bit

 

是否偏向鎖

鎖標誌位

偏向鎖

ThreadId==0

epoch==n

分代年齡

1

01

 

     以上三個點都判斷通過,進入“第二步”,加鎖流程

第二步:通過CAS原子操作,把T1的ThreadId寫入MW。執行結果有兩種情況:

  1. 寫入成功,獲得偏向鎖,進入同步程式碼塊執行同步邏輯。
  2. 寫入失敗,表明在第一步判斷和CAS操作之間,有其他執行緒已獲得了鎖。走鎖競爭邏輯。

2.解鎖過程

當前執行緒執行完同步程式碼塊後,進行解鎖,解鎖操作比較簡單,僅僅將棧中的最近一條LR中的obj賦值為null。這裡需要注意,MW中的threadId並不會做修改。

 

 

3.鎖競爭處理流程

  持有鎖的執行緒T2並不會在發現競爭的第一時間就直接撤銷鎖,或者升級鎖,而是執行到安全點後再處理。

  1. 此時如果當前執行緒已執行完同步塊程式碼且執行緒已不存活,將會撤銷鎖,將鎖物件恢復至無鎖狀態,然後進入鎖升級邏輯。
  2. 如果當前執行緒同步塊還未執行完或者執行緒依然存活,將會走鎖升級流程,升級為輕量級鎖,且升級完後T2繼續持有輕量級鎖,繼續執行同步程式碼。

 

 

 

  ps:怎麼判斷是否還在執行同步程式碼呢?遍歷棧中的RL,如果都為null,代表鎖已全部釋放。

4.批量重偏向和批量撤銷

有這樣一種場景:如果我們預判競爭不多,大部分情況下是單一執行緒執行同步塊,開啟了偏向鎖。但是在實際使用環境中,出現了大量的競爭,這時候怎麼辦呢?停機重新配置引數?恐怕不是最好的方案。如果是我們來設計這個這個Synchronized鎖,肯定也會做一些兜底策略。比如這樣來做,當某一事件發生了N次,那麼就更改一下處理策略?

是的,基本思想差不多,只不過更完善,暫時留一個懸念,在下次揭曉。

 

&nbs