深入分析 synchronized 關鍵字

weixin_33806914發表於2019-03-04

synchronized 概述

synchronized 關鍵字提供了一種獨佔式的加鎖方式,用來控制多個執行緒對共享資源的互斥訪問。它可以保證在同一時刻只有一個執行緒在執行該段程式碼,同時它還可以保證共享變數的記憶體可見性。

  • 互斥性:同一時刻只允許一個執行緒持有某個物件鎖,一次實現對共享資源的互斥訪問。
  • 可見性:確保在鎖釋放前,對共享變數做的修改,對隨後獲得該鎖的另一個執行緒是可見的。

synchronized 的獲取和釋放鎖由 JVM 實現,使用者不需要顯示的獲取和釋放鎖,非常方便。但是當執行緒嘗試獲取鎖的時候,如果獲取不到鎖該執行緒會一直阻塞。

在早期版本中,synchronized 是一個重量級鎖,效率低下。但從 JDK1.6 開始,從 JVM 層面對 synchronized 引入了各種鎖優化技術,例如:自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、輕量級鎖和偏向鎖等,大大減少了鎖操作的開銷。

使用方式

使用 synchronized 實現同步有同步方法塊、同步方兩種方式。

同步方法塊

作用於程式碼塊時,括號中可以是指定的物件,也可以是 Class 物件。

// 鎖的是指定的物件例項
public void test1 (){
    synchronized(this) {
        // ···
    }
}
複製程式碼
// 鎖定是指定的類物件
public void test2 (){
    synchronized(Test.class) {
        // ···
    }
}
複製程式碼

同步方法

作用於方法時,鎖的是當前的物件例項。

public synchronized void test3(){
    // ···
}
複製程式碼

作用於靜態方法,鎖的是類物件。

public synchronized static void test4(){
    // ···
}
複製程式碼

原理基礎

HotSpot 物件頭

HotSpot 虛擬機器的物件頭分為兩部分資訊:

  • Mark Word:用於存放物件自身的執行時資料,如雜湊碼、GC 分代年齡、鎖型別、鎖標誌位等資訊,這部分資料在 32 位和 64 位虛擬機器中分別為 3264 bit。它是實現輕量級鎖和偏向鎖的關鍵。
  • Class Metadata Address:用於儲存指向方法區物件型別資料的指標,如果是陣列,還會有一個額外的部分用於存放陣列長度。

Mark Word 被設計為一個非固定的資料結構以便儲存更多的資訊,它會根據物件的狀態複用自己的儲存空間。例如,在 32 位的 HotSpot 虛擬機器中,各種狀態下物件的儲存內容如下:

Monitor

每個 Java 物件都有一個 Monitor 物件與之關聯,它被稱為管程(監視器鎖),前面的表格中,鎖狀態為重量級鎖時,指標就指向 Monitor 物件的起始地址。當一個 Monitor 被某個執行緒持有後,便處於鎖定狀態。在 HotSpot 虛擬機器的原始碼實現中,ObjectMonitor 物件相關屬性有:

  • _count:計數器;
  • _owner:指向持有 ObjectMonitor 物件的執行緒;
  • _WaitSet:等待池;
  • _EntryList:鎖池;

多個執行緒訪問同步程式碼時,首先會進入 _EntryList 鎖池中被阻塞,當執行緒獲取到物件的 Monitor 後,就會把 _owner 指向當前執行緒,同時 Monitor 中的 _count 計數器加一。如果執行緒呼叫 wait 方法,_owner 就被恢復為 null_count 計數器減一,同時該執行緒就會進入 _WaitSet 等待池中。

當執行緒執行完畢,將對應的變數復位,以便其他執行緒獲取 Monitor 鎖。

四種狀態

synchronized 有四種狀態:無鎖、偏向鎖、輕量級鎖和重量級鎖。隨著對鎖的競爭逐漸激烈,鎖的狀態進行升級。

synchronized 實現原理

同步方法塊

public class SynchronizedTest {
    public void test1() {
        synchronized (this) {
            // ···
        }
    }
}
複製程式碼

使用 javap -c -vSynchronizedTest.class 進行反彙編:

可以看到,在同步程式碼塊的開始位置插入 monitorenter 指令,在結束位置插入 monitorexit 指令,而且必須保證每一個 monitorenter 都有一個 monitorexit 與之對應。

synchronized 便是通過 Monitor 獲取鎖的。當執行緒執行到 monitorenter 指令時,將會嘗試獲取 Monitor 所有權。當計數器為 0,則成功獲取;獲取後將鎖計數器置為 1。在執行 monitorexit 指令時,將鎖計數器置為 0。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到鎖被另外一個執行緒釋放為止。

同步方法

public class SynchronizedTest {
    public synchronized void test1() {
        // ···
    }
}
複製程式碼

使用 javap -c -vSynchronizedTest.class 進行反彙編:

可以看到,被同步的方法也僅是被翻譯成普通的方法呼叫和返回指令。在 JVM 位元組碼層面並沒有任何特別的指令來實現 synchronized 修飾的方法。

但是在 Class 檔案的方法表中將方法的 flags 欄位中的 ACC_SYNCHRONIZED 標誌位置為 1,表示該方法是同步方法。在執行方法時,執行緒就會持有 Monitor 物件。

底層優化

JDK1.6 對鎖引入了大量的優化,如自旋鎖、自適應自旋鎖、鎖消除、鎖粗化、輕量級鎖、偏向鎖等技術來減少鎖操作的開銷。

自旋鎖和自適應自旋

在實現同步互斥時,如果獲取鎖失敗,就會使當前執行緒阻塞,但執行緒的掛起和恢復都需要在核心態和使用者態之間轉換,對系統的效能影響很大。許多情況下共享資料的鎖定狀態持續時間不會很長,切換執行緒不值得。

自旋鎖就是讓執行緒在請求共享資料的鎖時執行一個忙迴圈(自旋),如果能夠很快獲得鎖,就避免其進入阻塞狀態。

自旋等待雖然避免了執行緒切換的開銷,但它要求多處理器,而且要佔用處理器時間。如果鎖佔用時間過長,那麼反而會消耗更多的資源。因此,對自旋等待的時間必須進行限制,另外自旋的次數也不能過多,預設為 10 次,可使用 -XX:PreBlockSpin 引數修改。

JDK1.6 中引入了自適應的自旋鎖,它的自旋時間由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

鎖消除

鎖消除是指虛擬機器的即時編譯器在執行時,如果程式碼要求同步,但檢測發現不可能存在共享資料競爭時,那麼就進行鎖消除。

鎖消除主要根據逃逸分析,如果判斷在一段程式碼中,堆上的所有資料都不會逃逸出去,那就可以將它們認為是執行緒私有的,也就無須進行同步加鎖。

鎖粗化

如果一系列的連續操作都對同一個物件反覆加鎖和解鎖,甚至加鎖也是出現迴圈體中,那麼即使沒有資料競爭,頻繁地加鎖解鎖也會導致不必須的效能消耗。

鎖粗化指的就是如果虛擬機器探測到這樣的情況,那就將加鎖同步的範圍擴充套件(粗化)到整個操作序列的外部。

偏向鎖

偏向鎖是在無競爭的情況下消除整個同步,也就是減少同一執行緒獲取鎖的代價。它的思想是這個鎖會偏向於第一個獲得它的執行緒,如果接下來該鎖沒有被其他的執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。

當鎖物件第一次被執行緒獲取時,鎖進入偏向模式,同時 Mard Word 的結構也變為偏向鎖結構。鎖標誌位為“01”,同時使用 CAS 操作把獲取到這個鎖的執行緒的 ID 記錄在物件的 Mark Word 中,如果 CAS 操作成功,這個執行緒以後每次進入這個鎖相關的同步塊時,都可以不用再進行任何同步操作。

不適用於鎖競爭比較激烈的多執行緒場合。

當有另外一個執行緒去嘗試獲取這個鎖物件時,偏向狀態就宣告結束。根據鎖物件目前是否處於被鎖定的狀態,撤銷偏向後恢復到未鎖定狀態或者輕量級鎖狀態。

輕量級鎖

輕量級鎖是相對於使用作業系統互斥量實現的傳統鎖而言的。偏向鎖執行在一個執行緒進入同步塊時,如果有第二個執行緒加入鎖競爭,則偏向鎖就會升級為輕量級鎖。它適用於執行緒交替執行的場景。

在程式碼進入同步塊時,如果此同步物件沒有被鎖定(鎖標誌位為“01”狀態),虛擬機器將先在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前 Mark Word 的拷貝。如下圖,左側是一個執行緒的虛擬機器棧,右側是一個鎖物件:

然後,虛擬機器將使用 CAS 操作嘗試將物件的 Mark Word 更新為指向 Lock Record 的指標,並將 Lock Record 裡的 owner 指標指向物件的 Mark Word。如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件的 Mark Word 的鎖標誌位轉變為“00”,即表示物件處於輕量級鎖定狀態。多執行緒堆疊和物件頭的狀態如下:

如果這個更新操作失敗了,虛擬機器首先會檢查物件的 Mark Word 是否指向當前執行緒的棧幀,如果已指向則說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,如果沒有指向則說明這個鎖物件已經被其他物件搶佔了。

如果有兩條以上的執行緒爭用同一個鎖,那輕量級鎖就要膨脹為重量級鎖,鎖標誌變為“10”,Mark Word 中儲存的就是指向重量級鎖的指標,後面等待鎖的執行緒也要進入阻塞狀態。

對於絕大部分的鎖,在整個同步週期內都是不存在競爭的。如果沒有競爭,輕量級鎖使用 CAS 操作避免了重量級鎖使用互斥量的開銷,提升了程式同步的效能。

偏向鎖、輕量級鎖的狀態轉化及物件 Mark Word 的關係如下:

參考資料

相關文章