淺析synchronized鎖升級的原理與實現

2023-07-11 15:00:47

背景

在多執行緒程式設計中,執行緒同步是一個關鍵的概念,它確保了多個執行緒對共用資源的安全存取。Java中的synchronized關鍵字是一種常用的執行緒同步機制,它不僅提供了互斥存取的功能,還具備鎖升級的特性。本文將深入探討synchronized的鎖升級原理和實現方式。
在jdk1.5(包含)版本之前,因為加鎖和釋放鎖的過程JVM的底層都是由作業系統mutex lock來實現的,其中會涉及上下文的切換(即使用者態和核心態的轉換),效能消耗極其高,所以在當時synchronized鎖是公認的重量級鎖。
後來JVM開發團隊為解決效能問題,在jdk1.5版本中加入了JUC並行包,包下開發了很多Lock相關的鎖,來解決同步的效能問題,同時也開始在後續的迭代版本中對synchronized鎖不斷的進行優化來提高效能,比如在jdk1.6版本中就引入了「偏向鎖」和「輕量級鎖」,通過鎖的升級來解決不同並行場景下的效能問題。
通常用使用synchronized方式加鎖影響效能,主要原因如下:

  1. 加鎖解鎖依賴JVM層的的額外操作完成。
  2. 重量級鎖是通過作業系統對執行緒的掛起和恢復來實現,涉及核心態和使用者態的切換

需要儲備的知識:java物件的記憶體佈局

注意:本文程式碼所使用的JDK版本是1.8,JVM虛擬機器器是64位元的HotSpot實現為準。

鎖的用法

synchronized是java的同步關鍵字,可以使共用資源序列的執行,避免多執行緒競爭導致的執行結果錯誤,使用方法有以下三種。

  1. 作用在類的普通方法(非靜態方法)上,鎖的是當前物件範例。
public synchronized void lockInstance() {
    System.out.println("鎖的是當前物件範例");
}
  1. 作用在類的靜態方法上,鎖的是當前類class。
public synchronized static void lockClass() {
    System.out.println("鎖的是當前類class");
}
  1. 作用在程式碼塊上,鎖的是指定的物件範例。
public void lockObject(Object obj) {
    synchronized (obj) {
        System.out.println("鎖的是指定的物件範例obj");
    }
}

原理分析

通過以上的用法,我們可以看到synchronized使用起來很簡單,那它究竟是怎麼做到執行緒間互斥存取的呢,底層原理及實現是怎樣的呢,接下來我們一一解答。
前一篇文章寫了java物件的記憶體佈局,裡面有一個關於物件頭Markword儲存的內容表格,在synchronized鎖的使用過程中就用到了,如下圖所示。

鎖的狀態

在jdk1.5版本(包含)之前,鎖的狀態只有兩種狀態:「無鎖狀態」和「重量級鎖狀態」,只要有執行緒存取共用資源物件,則鎖直接成為重量級鎖,jdk1.6版本後,對synchronized鎖進行了優化,新加了「偏向鎖」和「輕量級鎖」,用來減少上下文的切換以提高效能,所以鎖就有了4種狀態。

  1. 無鎖

對於共用資源,不涉及多執行緒的競爭存取。

  1. 偏向鎖

共用資源首次被存取時,JVM會對該共用資源物件做一些設定,比如將物件頭中是否偏向鎖標誌位置為1,物件頭中的執行緒ID設定為當前執行緒ID(注意:這裡是作業系統的執行緒ID),後續當前執行緒再次存取這個共用資源時,會根據偏向鎖標識跟執行緒ID進行比對是否相同,比對成功則直接獲取到鎖,進入臨界區域(就是被鎖保護,執行緒間只能序列存取的程式碼),這也是synchronized鎖的可重入功能。

  1. 輕量級鎖

當多個執行緒同時申請共用資源鎖的存取時,這就產生了競爭,JVM會先嚐試使用輕量級鎖,以CAS方式來獲取鎖(一般就是自旋加鎖,不阻塞執行緒採用迴圈等待的方式),成功則獲取到鎖,狀態為輕量級鎖,失敗(達到一定的自旋次數還未成功)則鎖升級到重量級鎖。

  1. 重量級鎖

如果共用資源鎖已經被某個執行緒持有,此時是偏向鎖狀態,未釋放鎖前,再有其他執行緒來競爭時,則會升級到重量級鎖,另外輕量級鎖狀態多執行緒競爭鎖時,也會升級到重量級鎖,重量級鎖由作業系統來實現,所以效能消耗相對較高。
這4種級別的鎖,在獲取時效能消耗:重量級鎖 > 輕量級鎖 > 偏向鎖 > 無鎖。

鎖升級

鎖升級是針對於synchronized鎖在不同競爭條件下的一種優化,根據鎖在多執行緒中競爭的程度和狀態,synchronized鎖可在無鎖、偏向鎖、輕量級鎖和重量級鎖之間進行流轉,以降低獲取鎖的成本,提高獲取鎖的效能。
通過下面這個命令,可以看到所有JVM引數的預設值。

java -XX:+PrintFlagsFinal -version

鎖升級過程

  1. 當JVM啟動後,一個共用資源物件直到有執行緒第一個存取時,這段時間內是處於無鎖狀態,物件頭的Markword裡偏向鎖標識位是0,鎖標識位是01。

  1. 從jdk1.6之後,JVM有兩個預設引數是開啟的,-XX:+UseBiasedLocking(表示啟用偏向鎖,想要關閉偏向鎖,可新增JVM引數:-XX:-UseBiasedLocking),-XX:BiasedLockingStartupDelay=4000(表示JVM啟動4秒後開啟偏向鎖,也可以自定義這個延遲時間,如果設定成0,那麼JVM啟動就開啟偏向鎖)。

當一個共用資源首次被某個執行緒存取時,鎖就會從無鎖狀態升級到偏向鎖狀態,偏向鎖會在Markword的偏向執行緒ID裡儲存當前執行緒的作業系統執行緒ID,偏向鎖標識位是1,鎖標識位是01。此後如果當前執行緒再次進入臨界區域時,只比較這個偏向執行緒ID即可,這種情況是在只有一個執行緒存取的情況下,不再需要作業系統的重量級鎖來切換上下文,提供程式的存取效率。
另外需要注意的是,由於硬體資源的不斷升級,獲取鎖的成本隨之下降,jdk15版本後預設關閉了偏向鎖。
如果未開啟偏向鎖(或者在JVM偏向鎖延遲時間之前)有執行緒存取共用資源則直接由無鎖升級為輕量級鎖,請看第3步。

  1. 如果未開啟偏向鎖(或者在JVM偏向鎖延遲時間之前),有執行緒存取共用資源則直接由無鎖升級為輕量級鎖,開啟偏向執行緒鎖後,並且當前共用資源鎖已經是偏向鎖時,再有第二個執行緒存取共用資源鎖時,此時鎖可能升級為輕量級鎖,也可能還是偏向鎖狀態,因為這取決於執行緒間的競爭情況,如有沒有競爭,那麼偏向鎖的效率更高(因為頻繁的鎖競爭會導致偏向鎖的復原和升級到輕量級鎖),繼續保持偏向鎖。如果有競爭,則鎖狀態會從偏向鎖升級到輕量級鎖,這種情況下輕量級鎖效率會更高。

當第二個執行緒嘗試獲取偏向鎖失敗時,偏向鎖會升級為輕量級鎖,此時,JVM會使用CAS自旋操作來嘗試獲取鎖,如果成功則進入臨界區域,否則升級為重量級鎖。
輕量級鎖是在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,嘗試拷貝鎖物件頭的Markword到棧幀的Lock Record,若拷貝成功,JVM將使用CAS操作嘗試將物件頭的Markword更新為指向Lock Record的指標,並將Lock Record裡的owner指標指向物件頭的Markword。若拷貝失敗,若當前只有一個等待執行緒,則可通過自旋繼續嘗試, 當自旋超過一定的次數,或者一個執行緒在持有鎖,一個執行緒在自旋,又有第三個執行緒來存取時,輕量級鎖就會膨脹為重量級鎖。

  1. 當輕量級鎖獲取鎖失敗時,說明有競爭存在,輕量級鎖會升級為重量級鎖,此時,JVM會將執行緒阻塞,直到獲取到鎖後才能進入臨界區域,底層是通過作業系統的mutex lock來實現的,每個物件指向一個monitor物件,這個monitor物件在堆中與鎖是關聯的,通過monitorenter指令插入到同步程式碼塊在編譯後的開始位置,monitorexit指令插入到同步程式碼塊的結束處和異常處,這兩個指令配對出現。JVM的執行緒和作業系統的執行緒是對應的,重量級鎖的Markword裡儲存的指標是這個monitor物件的地址,作業系統來控制核心態中的執行緒的阻塞和恢復,從而達到JVM執行緒的阻塞和恢復,涉及核心態和使用者態的切換,影響效能,所以叫重量級鎖。


鎖升級簡要步驟如下所示

注意:圖中無鎖到偏向鎖這不是升級,是在偏向鎖開啟後,物件預設是偏向狀態,沒有從無鎖升級到偏向鎖的過程。偏向鎖未開啟,會直接從無鎖升級到輕量級鎖,偏向鎖開啟時,會從偏向鎖升級到輕量級鎖。

鎖升級細化流程

下面我們結合程式碼看下各狀態鎖的升級場景
需要新增JOL包,用來檢視物件頭資訊

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

無鎖 --> 輕量級鎖

無鎖升級到輕量級鎖有兩種情況

  1. 第一種,關閉偏向鎖,執行時增加JVM引數:-XX:-UseBiasedLocking
public void lockUpgradeTest1() {
    Object obj = new Object();
    System.out.println("未開啟偏向鎖,物件資訊");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj) {
        System.out.println("已獲取到鎖資訊");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("已釋放鎖資訊");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

執行結果:

未開啟偏向鎖,物件資訊
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

已獲取到鎖資訊
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000000336f2b0 (thin lock: 0x000000000336f2b0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

已釋放鎖資訊
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

採用JOL輸出的物件頭markword是16進位制的,需要轉換成64位元的2進位制來看。

關閉偏向鎖的情況下,物件加鎖之前,物件頭markword是0x0000000000000001換算成二進位制末尾三位是001,即偏向鎖標識為0,鎖標識為01,是無鎖狀態。
加鎖成功後,執行同步程式碼塊,物件頭markword是0x000000000336f2b0換算成二進位制末尾兩位是00,即鎖標識為00,是輕量級鎖狀態。
最後在執行完同步程式碼塊後,再次列印物件頭資訊,物件頭markword是0x0000000000000001換算成二進位制末尾三位是001,即偏向鎖標識為0,鎖標識為01,是無鎖狀態,說明輕量級鎖在執行完同步程式碼塊後進行了鎖的釋放。

  1. 第二種,預設情況下,在偏向鎖延遲時間之前獲取鎖
public void lockUpgradeTest2() {
    Object obj = new Object();
    System.out.println("開啟偏向鎖,偏向鎖延遲時間前,物件資訊");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj) {
        System.out.println("已獲取到鎖資訊");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("開啟偏向鎖,已釋放鎖資訊");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

執行結果:

開啟偏向鎖,偏向鎖延遲時間前,物件資訊
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

已獲取到鎖資訊
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000000316f390 (thin lock: 0x000000000316f390)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

開啟偏向鎖,已釋放鎖資訊
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

使用預設的偏向鎖設定,JVM啟動4秒後才啟動偏向鎖,所以JVM啟動時就列印並獲取鎖資訊,效果跟第一種一樣,markword解釋同上。

偏向鎖 --> 輕量級鎖

public void lockUpgradeTest3() {
    // JVM預設4秒後才可以偏向鎖,所以這裡休眠5秒,鎖物件就是偏向鎖了
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "開啟偏向鎖,偏向鎖延遲時間後,物件資訊" + ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "已獲取到鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(Thread.currentThread().getName() + "開啟偏向鎖,已釋放鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
    }, "t1");
    t1.start();
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    Thread t2 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "開啟偏向鎖,偏向鎖延遲時間後,物件資訊" + ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "已獲取到鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(Thread.currentThread().getName() + "開啟偏向鎖,已釋放鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
    }, "t2");
    t2.start();
}

執行結果有兩種可能:
第一種:

t1開啟偏向鎖,偏向鎖延遲時間後,物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001fbb3005 (biased: 0x000000000007eecc; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1開啟偏向鎖,已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001fbb3005 (biased: 0x000000000007eecc; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2開啟偏向鎖,偏向鎖延遲時間後,物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001fbb3005 (biased: 0x000000000007eecc; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020f7f2d0 (thin lock: 0x0000000020f7f2d0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2開啟偏向鎖,已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

啟動JVM,預設4秒後開啟偏向鎖,這裡休眠了5秒,保證JVM開啟偏向鎖,然後建立了物件,物件頭markword資訊0x0000000000000005換算成二進位制後三位是101,偏向鎖標識為1,鎖標識為01,為偏向鎖狀態,偏向執行緒ID是0,說明這是初始偏向狀態,t1先獲取到鎖進入同步程式碼塊後,markword變成0x000000001fbb3005轉換成二進位制:11111101110110011000000000101(前面補0直到長度是64位元),末尾三位依然是101,還是偏向鎖,只不過前54位元將對應的作業系統執行緒ID寫到偏向執行緒ID裡了,同步程式碼塊執行完成後,markword依然沒變,說明偏向鎖狀態不會自動釋放鎖,需要等其他執行緒來競爭鎖才走偏向鎖復原流程。t2執行緒開始執行時鎖物件markword是0x000000001fbb3005,說明偏向鎖偏向了t1對應的作業系統執行緒,等t1釋放鎖,t2獲取到鎖進入同步程式碼塊時,物件鎖markword是0x0000000020f7f2d0,換算成二進位制:100000111101111111001011010000(前面補0直到長度是64位元),末尾兩位是00,鎖已經變成輕量級鎖了,鎖的指標也變了,是指向t2執行緒棧中的Lock Record記錄了,等t2執行緒釋放鎖後,物件鎖末尾是001,說明是無鎖狀態了,輕量級鎖會自動釋放鎖。
第二種:

t1開啟偏向鎖,偏向鎖延遲時間後,物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1開啟偏向鎖,已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2開啟偏向鎖,偏向鎖延遲時間後,物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2開啟偏向鎖,已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1執行緒正常獲取鎖,鎖狀態是偏向鎖,執行完同步程式碼塊後鎖還是偏向鎖,說明偏向鎖不隨執行同步程式碼塊的結束而釋放鎖,t2執行緒拿到鎖是偏向鎖,獲取到鎖依然是偏向鎖,而沒有升級到輕量級鎖,說明執行緒間鎖沒有競爭的情況下,依然保持偏向鎖,這樣效率會更高。

偏向鎖 --> 重量級鎖

public void lockUpgradeTest4() {
    // JVM預設4秒後才可以偏向鎖,所以這裡休眠5秒,鎖物件就是偏向鎖了
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "加鎖前物件資訊" + ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "已獲取到鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
            try {
                // 讓t2執行緒啟動後並競爭鎖
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName() + "已釋放鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
    }, "t1");
    t1.start();
    try {
        // 讓t1執行緒先啟動並拿到鎖
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    Thread t2 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "加鎖前物件資訊" + ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "已獲取到鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(Thread.currentThread().getName() + "已釋放鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
    }, "t2");
    t2.start();
}

執行結果:

t1加鎖前物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020993805 (biased: 0x000000000008264e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2加鎖前物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020993805 (biased: 0x000000000008264e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d2c6c2a (fat lock: 0x000000001d2c6c2a)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d2c6c2a (fat lock: 0x000000001d2c6c2a)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

程式先休眠5秒保證偏向鎖開啟,然後t1執行緒先啟動併成功獲取到鎖,t1獲取到鎖之前物件markword是偏向狀態但偏向執行緒ID是0,t1獲取到鎖之後markword裡有了偏向執行緒ID,也就是t1執行緒對應的作業系統執行緒ID。t2執行緒獲取鎖之前,物件鎖已經是偏向鎖並偏向t1對應的執行緒,t2執行緒獲取鎖時t1已經持有鎖並沒有釋放,鎖未釋放其他執行緒再競爭鎖,這時會發生鎖升級,由偏向鎖升級成重量級鎖,所以t1釋放鎖跟t2獲取到鎖時,物件頭的markword是0x000000001d2c6c2a,轉換成二進位制11101001011000110110000101010(前後補0到夠64位元),最後兩位是10,標識重量級鎖,前面的62存的是指向堆中跟monitor對應鎖物件的指標。

輕量級鎖 --> 重量級鎖

public void lockUpgradeTest5() {
        // JVM預設4秒後才可以偏向鎖,所以這裡休眠5秒,鎖物件就是偏向鎖了
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "加鎖前物件資訊" + ClassLayout.parseInstance(object).toPrintable());
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "已獲取到鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
                try {
                    // 讓t2執行緒啟動後並競爭鎖
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + "已釋放鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
        }, "t1");
        t1.start();
        try {
            // 讓t1執行緒先啟動並拿到鎖
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        Thread t2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "加鎖前物件資訊" + ClassLayout.parseInstance(object).toPrintable());
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "已獲取到鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
            }
            System.out.println(Thread.currentThread().getName() + "已釋放鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
        }, "t2");
        t2.start();
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "加鎖前物件資訊" + ClassLayout.parseInstance(object).toPrintable());
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + "已獲取到鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
                }
                System.out.println(Thread.currentThread().getName() + "已釋放鎖資訊" + ClassLayout.parseInstance(object).toPrintable());
            }, "t3_" + i).start();
        }

    }

執行結果:

注意:這裡t2執行緒也有可能獲取到的鎖是偏向鎖,無競爭的情況下,這取決於執行緒的執行情況。這裡我們以t2獲取到輕量級鎖,講解輕量級鎖升級到重量級鎖的過程。

t1加鎖前物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2加鎖前物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_0加鎖前物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_1加鎖前物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002270f1d0 (thin lock: 0x000000002270f1d0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_2加鎖前物件資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002270f1d0 (thin lock: 0x000000002270f1d0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_1已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_1已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_0已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_0已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_2已獲取到鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_2已釋放鎖資訊java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1執行緒加鎖執行程式碼塊後,鎖狀態是偏向鎖,t1在同步程式碼塊裡讓休眠了3秒目的是讓t2執行緒起來並競爭鎖,然後t1執行緒執行完同步程式碼塊,鎖狀態還是偏向鎖,這時候for迴圈的3個執行緒也啟動起來爭搶所,t2執行緒先啟動獲取到鎖為輕量級鎖,for迴圈裡啟動的3個執行緒在獲取同步鎖前,我們看到列印的鎖狀態有的是偏向鎖、有的是輕量級鎖,說明在t2執行緒加鎖成功前還是偏向鎖,t2加鎖後就成輕量級鎖了,然後for迴圈的3個執行緒相繼獲取到鎖,發現鎖已經升級到重量級鎖了,物件頭markword是0x000000001d0356da,換成二進位制:11101000000110101011011011010(前面補齊0到夠64位元),末尾兩位鎖狀態是10,表示重量級鎖。

底層實現

本文開頭講的synchronized在程式碼層的用法有三種,鎖物件範例、鎖類class、鎖指定範例物件,我們可以將以下程式碼編譯成class後,在反編譯出來看看JVM指令碼是怎樣的。

public class Synchronized1 {
    public static void main(String[] args) {
        System.out.println("test Synchronized1");
    }
    public synchronized void lockInstance() {
        System.out.println("鎖的是當前物件範例");
    }

    public synchronized static void lockClass() {
        System.out.println("鎖的是當前類class");
    }

    public void lockObject(Object obj) {
        synchronized (obj) {
            System.out.println("鎖的是指定的物件範例obj");
        }
    }
}

通過javap命令反編譯class檔案。

我本文的例子使用的命令是這樣的:

javap -c -v -l Synchronized1.class

我們主要關注那3個方法的JVM指令碼。


在方法(非靜態方法鎖的是物件,靜態方法鎖的是類class)上加synchronized關鍵字,是通過在access_flags中設定ACC_SYNCHRONIZED標誌來實現,synchronized使用在程式碼塊上,是通過monitorenter和monitorexit指令來實現。
重量鎖底層最終是依靠作業系統的阻塞和喚醒來實現,每個物件有一個監視器鎖(monitor),在 Java 虛擬機器器(HotSpot)中,monitor 是基於 C++的ObjectMonitor實現,物件鎖裡有計數器、重入次數、等待鎖的執行緒列表、儲存該monitor的物件、擁有monitor的執行緒等引數,虛擬機器器是通過進入和退出monitor來實現同步,monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,monitorexit是插入到方法結束處和異常處。根據虛擬機器器規範的要求,在執行monitorenter指令時,首先要去嘗試獲取物件的鎖,如果這個物件沒被鎖定,或者當前執行緒已經擁有了那個物件的鎖,把鎖的計數器加1;相應地,在執行monitorexit指令時會將鎖計數器減1,當計數器被減到0時,鎖就釋放了(注意執行monitorexit的執行緒必須是已經獲得monitor物件鎖的執行緒)。如果獲取物件鎖失敗了,那當前執行緒就要阻塞等待,直到物件鎖被另一個執行緒釋放然後由作業系統喚醒等到鎖的執行緒繼續競爭鎖直到獲取到鎖為止。

總結

synchronized關鍵字是Java中常用的執行緒同步機制,其具備鎖升級的特性,可以根據競爭的程度和鎖的狀態進行自動切換。鎖升級通過無鎖、偏向鎖、輕量級鎖和重量級鎖四種狀態的轉換,以提高並行效能。在實際開發中,我們應該瞭解鎖升級的原理,並根據具體場景進行合理的鎖設計和優化,以實現高效且安全的多執行緒程式設計。
隨著jdk版本的升級,JVM底層的實現持續優化,版本的不同伴隨著引數使用及預設設定的不同,但總之JVM層對synchronized的優化效率越來越高,所以不應該再把synchronized同步當重量級鎖來看。
其實本文介紹了鎖升級的主要過程,關於synchronized還有鎖消除、鎖粗化的優化手段,使得synchronized效能在某些場景應用下,可能會比JUC包底下的Lock相關鎖效率更高。
另外synchronized鎖原理、優化、使用遠不止本文說的這麼多,感興趣的可進一步探索。