【併發程式設計】(二)Java併發機制底層實現原理——synchronized關鍵字

sun_tantan發表於2021-01-02

synchronized定義

synchronized
Java語言的關鍵字,可用來給物件和方法或者程式碼塊加鎖,當它鎖定一個方法或者一個程式碼塊的時候,同一時刻最多隻有一個執行緒執行這段程式碼。
當兩個併發執行緒訪問同一個物件object中的這個加鎖同步程式碼塊時,一個時間內只能有一個執行緒得到執行。另一個執行緒必須等待當前執行緒執行完這個程式碼塊以後才能執行該程式碼塊。
然而,當一個執行緒訪問object的一個加鎖程式碼塊時,另一個執行緒仍可以訪問該object中的非加鎖程式碼塊。

JMM中最重要的問題:重排序、原子性、可見性。而volatile、synchronized、final、和Lock都可以保證可見性:

  • volatile:通過禁止違背Happends-before原則的重排序操作,和變數改變時立即重新整理值到主存,來保證。
  • synchronized和lock:都是通過把併發的過程強制轉化為原子化的過程;
  • final:變數本來就是放在多個執行緒共享的區域,自然可見;

synchronized實現原理

物件頭

在HotSpot虛擬機器中,java物件在虛擬機器中的儲存佈局分為3塊區域:物件頭、例項資料和對齊填充;而synchronized用的鎖就是存在java物件頭裡的。

  • java物件頭:
長度內容說明
32/64bitMark Word儲存物件的hashcode或鎖資訊
32/64bitClass Metadata Address儲存物件型別資料的指標
32/64bitArray Length陣列的長度(如果當前物件是陣列)
  • 32位的JVM的Mark Word中的預設儲存結構無鎖狀態如下:
鎖狀態25bit4bit1bit是否是偏向鎖2bit鎖標誌位
無鎖狀態物件的hashcode物件分代年齡001
  • 在執行期間,Mark word中儲存的資料會隨著鎖標誌位的變化而變化:
鎖狀態25bit4bit1bit是否是偏向鎖2bit鎖標誌位
偏向鎖執行緒ID I Epoch物件分代年齡101
鎖狀態25bit I 4bit I 1bit是否是偏向鎖2bit鎖標誌位
輕量級鎖指向棧中鎖記錄的指標00
鎖狀態25bit I 4bit I 1bit是否是偏向鎖2bit鎖標誌位
重量級鎖指向重量級鎖的指標10

鎖分類

Java SE 1.6為了減少獲取鎖和釋放鎖帶來的效能消耗,引入了偏向鎖和輕量級鎖。在Java SE 1.6中鎖一共有4種狀態:

  • 無鎖 ——> 偏向鎖 ——> 輕量級鎖 ——> 重量級鎖

這幾個狀態會隨著競爭情況逐漸升級,鎖可以升級但不能降級。

1. 偏向鎖

HotSpot的作者發現,大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得;當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒id。

以後該執行緒在進入和退出同步塊時不需要進行CAS來加鎖和解鎖;只需要簡單地測試一下物件頭中是否儲存著指向當前執行緒的偏向鎖。

  • 偏向鎖的撤銷:當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放。
  • 偏向鎖的優點:當只有一個執行緒執行同步塊時,不需要CAS操作直接獲取鎖,提升效能;
  • 適用於一個執行緒反覆獲取同一個鎖的情況。

2. 輕量級鎖

輕量級鎖,也叫自旋鎖:
執行緒在執行同步塊之前,JVM會在當前的 棧幀 中建立Lock Record用於儲存鎖記錄的空間,將物件頭中的Mark Word複製到Lock Record,官方稱為Displaced Mark Word。

執行緒嘗試使用CAS將物件頭中的Mark Word替換為 指向鎖記錄的指標

  • 如果成功,當前執行緒獲取鎖;
  • 如果失敗,表示其他執行緒競爭鎖,當前執行緒嘗試使用 自旋 來獲取鎖。

輕量鎖的解鎖:使用原子的CAS將Displaced Mark Word替換回到物件頭:

  • 如果成功,表示沒有競爭發生;
  • 如果失敗,表示當前鎖存在競爭,鎖就會膨脹為重量級鎖。

3. 重量級鎖

升級為重量級鎖後,Mark Word中儲存的是指向重量級鎖的指標,此時等待鎖的執行緒都會進入到 阻塞 狀態。
Synchronzed的重量級鎖是通過物件內部的一個監視器鎖 Monitor 來實現的。而Monitor又是依賴於底層作業系統的Mutex Lock互斥鎖實現的。而作業系統實現執行緒之間的切換需要從使用者態轉為核心態,成本很高。

  • Monitor:同一個時刻,只有一個程式或者執行緒可以進入monitor中定義的臨界區,這使得monitor可以達到互斥的效果。而無法進入臨界區的程式或執行緒,它們應該被阻塞,並且在必要的時候被喚醒。
  • Monitor需要的元素:
    • 臨界區
    • monitor物件及鎖
    • 條件變數及定義在monitor物件上的wait、signal操作

在Java語言中,我們知道使用synchronized關鍵字時,總是需要指定一個物件與之關聯,比如this或者this.Class物件。這個物件就是 Monitor Object ,即java.lang.Object。Object物件提供了wait()、notify()等方法對monitor機制進行了封裝。

  • Java Monitor原理:
    在這裡插入圖片描述
  1. 當執行緒需要獲取一個物件的鎖時,會被放入EntrySet進行等待;
  2. 此執行緒獲取了鎖,成為鎖的擁有者,進入到The Owner區域;
  3. 擁有者執行緒可以呼叫wait()釋放鎖,進入WaitSet進入等待狀態;
  4. 其他執行緒呼叫notify()喚醒等待集合中的執行緒

鎖升級過程

  1. 當前沒有多執行緒的競爭,只有一個執行緒去獲取鎖, 進入偏向鎖模式 (在物件頭中儲存執行緒ID)
  2. 另一個執行緒來爭取鎖:判斷物件是否為偏向狀態(此時為是)、判斷物件頭中儲存的執行緒id是否為本執行緒id(此時不是):那麼嘗試通過CAS設定為當前執行緒ID
    • 成功(因為上一個持有偏向鎖的執行緒是不會主動釋放的),獲取偏向鎖並執行程式碼塊;
    • 失敗(表示有執行緒競爭的情況存在),撤銷偏向鎖 模式,準備升級輕量級鎖:
      • 在棧幀中開闢一片鎖記錄空間,用於儲存當前物件Mark Word的拷貝;
      • 使用CAS操作嘗試把物件的Mark Word更新為指向此執行緒的棧幀鎖記錄指標。
        CAS成功,表示成功獲取鎖:將 鎖模式設定為輕量級鎖
        CAS失敗,採用 自旋 方式不斷重試
  3. 自旋當超出一定次數時,表示競爭激烈, 直接升級為重量級鎖 。在此狀態下,所有等待的執行緒都進入到阻塞狀態,而不再CAS重試。

相關文章