多執行緒知識梳理(3) synchronized 三部曲之鎖優化

澤毛發表於2017-12-21

一、前言

多執行緒知識梳理(2) - synchronized 基本使用 中,我們介紹了使用重量鎖來實現的synchronized。今天,我們就來一起學習一下在JDK 1.6之後,對synchronized所採取的一系列優化措施。

二、物件頭 & Monitor Record

在介紹優化方法之前,我們需要介紹兩個重要的概念Java物件頭和Monitor

多執行緒知識梳理(3)   synchronized 三部曲之鎖優化

2.1 物件頭

Java&Android 基礎知識梳理(3) - 記憶體區域 中介紹記憶體區域的時候,對於一個Java物件所佔的記憶體區域是這麼介紹的:

多執行緒知識梳理(3)   synchronized 三部曲之鎖優化
在執行過程中,物件頭所包含資料的含義不是固定不變的,隨著鎖狀態標誌位(下圖中紅框的範圍)的改變,其它欄位所表示的含義也不同,以32位的虛擬機器為例,下圖就是鎖狀態標誌位所對應的資料結構含義:
多執行緒知識梳理(3)   synchronized 三部曲之鎖優化

2.2 Monitor Record

Monitor是執行緒私有的資料結構,由於一個執行緒可能進入多個不同的同步方法,這些方法有可能會關聯到不同的Monitor,因此每一個執行緒都有一個可用的Monitor列表,同時還有一個全域性的可用列表,Monitor資料結構包括以下成員變數:

  • Owner:初始時為空表示當前沒有任何執行緒擁有該Monitor,當執行緒成功擁有該鎖後儲存執行緒唯一標識,當鎖被釋放時又設定為空。
  • EntryQ:關聯一個系統互斥鎖,阻塞所有試圖獲得Monitor但是最終失敗了的執行緒。
  • RcThis:表示blockedwaiting在該Monitor上的所有執行緒的個數。
  • Nest:用來實現重入鎖的計數。
  • HashCode:儲存從物件頭拷貝過來的HashCode值。
  • Candidate:用來避免不必要的阻塞或等待執行緒喚醒,因為每一次只有一個執行緒能夠成功擁有鎖,如果每次前一個釋放鎖的執行緒喚醒所有正在阻塞或等待的執行緒,會引起不必要的上下文切換(從阻塞到就緒然後因為競爭鎖失敗又被阻塞)從而導致效能嚴重下降。Candidate只有兩種可能的值:0表示沒有需要喚醒的執行緒,1表示要喚醒一個繼任執行緒來競爭鎖。

三、實現優化

JDK 1.6之後,它對於鎖進行了一系列的優化措施,主要包括:自適應自旋鎖、鎖消除和鎖粗化。

3.1 自旋鎖

由於執行緒的阻塞和喚醒需要CPU從使用者態轉換成核心態,而頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作。

因此,我們在發現鎖已經被其它執行緒佔有時,並不直接讓當前執行緒進入阻塞狀態,而是讓執行緒執行一段無意義的迴圈,待迴圈結束後,如何仍然無法獲取到鎖,那麼才進入阻塞狀態。

決定自旋鎖效能的關鍵在於自旋次數的選擇,在JDK 1.6之後,引入了自適應自旋鎖,它會根據前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定新的自旋次數。

3.2 鎖消除

JVM檢測到不可能存在共享資料競爭,會對同步鎖進行鎖消除。

3.3 鎖粗化

在使用同步鎖的時候,需要讓同步塊的作用範圍儘可能地小,僅在共享資料的實際作用域中才進行同步,這樣做的目的是為了使需要同步的運算元量儘可能縮小,如果存在鎖競爭,那麼等待鎖的執行緒也能儘快拿到鎖。

然而,如果一系列連續加鎖解鎖操作,可能會導致不必要的效能損耗,所以有時可以將多個連續的加鎖、解鎖操作連線在一起,擴充套件成一個範圍更大的鎖。

四、狀態優化

JDK 1.6之前,鎖只有兩種狀態:無鎖狀態和重量級鎖狀態,而在這之後增加為四種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,這種改進基於兩點考慮:

  • 無鎖狀態和重量級鎖狀態之間的切換是依賴於底層作業系統的Mutex Lock實現,作業系統實現執行緒之間的切換需要從使用者態切換到核心態,切換成本很高。
  • 實驗研究發現,對於絕大部分的鎖,在整個生命週期內都是不存在競爭的。

需要注意,對於鎖的這四種狀態,它們會隨著競爭的激烈而逐漸升級,但是它只允許鎖升級,不允許鎖降級。

無鎖狀態和重量級鎖狀態都比較好理解,下面我們主要介紹新增的兩種鎖狀態:偏向鎖狀態輕量級鎖狀態

整個轉換的流程圖如下所示,在後面的介紹中可以參考:

多執行緒知識梳理(3)   synchronized 三部曲之鎖優化

4.1 偏向鎖狀態

引入偏向鎖的目的是:在無多執行緒競爭的情況下,儘量減少不必要的輕量級鎖執行路徑,它的理想情況下是在無競爭時把整個同步都去掉,連CAS操作都省略。

偏向鎖的意思是這個鎖會偏向於第一個獲得它的執行緒,如果在接下來的執行過程中,該鎖沒有被其它執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。

4.1.1 獲取偏向鎖

(a) 前提條件

獲取偏向鎖的前提條件是synchronized所修飾的物件處於可偏向狀態

  • 鎖狀態為01
  • 偏向鎖狀態為1

(b) 獲取過程

當滿足前提條件時,再去判斷物件的Mark Word中的執行緒ID是否指向當前執行緒

  • 如果不指向當前執行緒,那麼通過CAS操作競爭鎖
    • 競爭成功:將Mark Word的執行緒ID替換為當前執行緒ID,接著執行同步程式碼塊
    • 競爭失敗:證明存在多執行緒競爭的情況,當到達全域性安全點,獲得偏向鎖的執行緒被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼塊
  • 如果指向當前執行緒,那麼執行同步程式碼塊

4.1.2 釋放偏向鎖

(a) 前提條件

釋放偏向鎖的前提條件是其它的執行緒在競爭偏向鎖的過程中出現了失敗的情況,並且偏向鎖的釋放需要等待到達全域性安全點。

(b) 釋放過程

當滿足釋放偏向鎖的前提條件時,首先會暫停擁有偏向鎖的執行緒,接著判斷鎖物件是否處於被鎖定的狀態,決定鎖標誌位下一步的狀態:

  • 如果未被鎖定,那麼將鎖標誌至為01,偏向鎖狀態置為0,表示它處於無鎖,且不可偏向狀態。
  • 如果已經被鎖定,那麼將鎖標誌置為00,表示它處於被輕量級鎖定的狀態。

4.2 輕量級鎖狀態

引入輕量級鎖的目的是:在無多執行緒競爭的情況下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗。

4.2.1 獲取輕量級鎖

(a) 前提條件

獲取輕量級鎖的前提條件時當前物件處於無鎖狀態,

  • 鎖狀態標誌位為01
  • 偏向鎖標誌位為0

(b) 獲取過程

JVM首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存物件目前的Mark Word的拷貝,之後JVM利用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標:

  • 操作成功:將鎖標誌置為00,表示處於鎖定的狀態,之後執行同步操作。
  • 操作失敗:那麼檢查物件的Mark Word是否指向當前執行緒的棧針
  • 如果是,則直接執行同步程式碼塊
  • 如果不是,說明該鎖物件已經被其他執行緒搶佔了,此時輕量級鎖升級為重量鎖,鎖標誌位變為10,後面等待的執行緒將會進入阻塞狀態。

4.2.2 釋放輕量級鎖

(a) 釋放過程

輕量級鎖的釋放也是通過CAS操作來進行的:

  • 取出在獲取輕量級鎖時,儲存在Displaced Mark Word中的資料。
  • CAS操作將取出的資料替換到當前物件的Mark Word中:
  • 如果成功,則說明釋放鎖成功
  • 如果失敗,說明有其它執行緒嘗試獲取該鎖,那麼需要在釋放鎖的同時,喚醒需要被喚醒的執行緒

對於輕量級鎖,它效能提升的依據是預設"對於絕大部分的鎖,在整個生命週期內是不會存在競爭的",如果不符合這種情況,那麼除了互斥的開銷外,還有額外的CAS操作,這樣輕量級鎖比重量級鎖更慢。

五、參考文章

Java 併發程式設計:Synchronized 底層優化(偏向鎖、輕量級鎖) 死磕 Java 併發 -----深入分析 synchronized 的實現原理

相關文章