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

来源:https://www.cnblogs.com/star95/archive/2023/07/11/17542850.html
-Advertisement-
Play Games

# 背景 在多線程編程中,線程同步是一個關鍵的概念,它確保了多個線程對共用資源的安全訪問。Java中的synchronized關鍵字是一種常用的線程同步機制,它不僅提供了互斥訪問的功能,還具備鎖升級的特性。本文將深入探討synchronized的鎖升級原理和實現方式。 在jdk1.5(包含)版本之前 ...


背景

在多線程編程中,線程同步是一個關鍵的概念,它確保了多個線程對共用資源的安全訪問。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鎖的使用過程中就用到了,如下圖所示。
image.png

鎖的狀態

在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。

image.png

  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步。
image.png

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

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

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

image.png
鎖升級簡要步驟如下所示
image.png

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

鎖升級細化流程
image.png
下麵我們結合代碼看下各狀態鎖的升級場景
需要添加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文件。
image.png
我本文的例子使用的命令是這樣的:

javap -c -v -l Synchronized1.class

我們主要關註那3個方法的JVM指令碼。
image.png

image.png

image.png
在方法(非靜態方法鎖的是對象,靜態方法鎖的是類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鎖原理、優化、使用遠不止本文說的這麼多,感興趣的可進一步探索。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 紅包分配問題 給你一個整數表示紅包的總額,和另一個整數表示紅包的個數 表示我們要把總金額,隨機分成N個紅包。 要求1:每個紅包的金額都是隨機的 要求2:每個人至少1分錢 示例代碼: 1 public class Test2 { 2 public static void main(String[] a ...
  • 瞭解驅動Docker的核心技術將讓您更深入地瞭解Docker的工作原理,並有助於您更有效地使用該平臺。 ### **Linux容器(LXC)** Linux容器(LXC)是Docker的基礎。 LXC是一種輕量級的虛擬化解決方案,允許多個隔離的Linux系統在單個主機上運行,無需全功能的虛擬化。 L ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 經歷了前面兩篇的入門和編譯源碼之後,從本 ...
  • # 一、使用JDBC批量添加 ## ​ 知識點複習: ​1、JDBC的六大步驟 (導入jar包, 載入驅動類,獲取連接對象, 獲取sql執行器、執行sql與並返回結果, 關閉資料庫連接) 2、​封裝了一個DBUtil 類, 通過讀取屬性文件的方式獲取 基礎連接信息。 3、​批量添加: 一次性可執行多 ...
  • 哈嘍兄弟們 我們平常需要下載文檔的時候,是不是發現,要麼不能下載,要麼不能複製,就能難受。 常見的文檔網站很多,但是這裡就不一一說名字了,emmm 那麼我們今天來分享一下,如何用Python將這些不給下載的文檔給批量下載下來。 你需要準備 開發環境 python 3.8 pycharm 模塊使用 兩 ...
  • 一. 介紹 1. 介面 Interface 介面可以稱之為一種規範,在Java中被用來定義一組方法,而不提供具體的實現細節,它規定了一個類應該要實現哪些方法;其他類可以通過實現介面來達到代碼重用和多態性的目的,幫助我們構建可擴展、靈活和可復用的代碼。 介面使用步驟: - 定義介面:使用關鍵字inte ...
  • 本文基於 Vert.x 官網 https://vertx.io/ 內容,帶領大家學習響應式編程里比較有名的工具包 Vert.x 。文章內容取自官網由博主簡化總結,希望幫助大家理解響應式編程。 - Vert.x 簡介 - Vert.x 特性 - 響應式模式概述 > 推薦博主開源的 H5 商城項目**w ...
  • poi-tl是一個基於Apache POI的Word模板引擎,也是一個免費開源的Java類庫。同類型的FreeMarker或Velocity基於文本模板和數據生成新的html頁面或配置文件。而poi tl是一個基於Word模板和數據生成新文檔的Word模板引擎。Word模板具有豐富的樣式。Poi-t... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...