# 背景 在多線程編程中,線程同步是一個關鍵的概念,它確保了多個線程對共用資源的安全訪問。Java中的synchronized關鍵字是一種常用的線程同步機制,它不僅提供了互斥訪問的功能,還具備鎖升級的特性。本文將深入探討synchronized的鎖升級原理和實現方式。 在jdk1.5(包含)版本之前 ...
背景
在多線程編程中,線程同步是一個關鍵的概念,它確保了多個線程對共用資源的安全訪問。Java中的synchronized關鍵字是一種常用的線程同步機制,它不僅提供了互斥訪問的功能,還具備鎖升級的特性。本文將深入探討synchronized的鎖升級原理和實現方式。
在jdk1.5(包含)版本之前,因為加鎖和釋放鎖的過程JVM的底層都是由操作系統mutex lock來實現的,其中會涉及上下文的切換(即用戶態和內核態的轉換),性能消耗極其高,所以在當時synchronized鎖是公認的重量級鎖。
後來JVM開發團隊為解決性能問題,在jdk1.5版本中加入了JUC併發包,包下開發了很多Lock相關的鎖,來解決同步的性能問題,同時也開始在後續的迭代版本中對synchronized鎖不斷的進行優化來提高性能,比如在jdk1.6版本中就引入了“偏向鎖”和“輕量級鎖”,通過鎖的升級來解決不同併發場景下的性能問題。
通常用使用synchronized方式加鎖影響性能,主要原因如下:
- 加鎖解鎖依賴JVM層的的額外操作完成。
- 重量級鎖是通過操作系統對線程的掛起和恢復來實現,涉及內核態和用戶態的切換
需要儲備的知識:java對象的記憶體佈局
註意:本文代碼所使用的JDK版本是1.8,JVM虛擬機是64位的HotSpot實現為準。
鎖的用法
synchronized是java的同步關鍵字,可以使共用資源串列的執行,避免多線程競爭導致的執行結果錯誤,使用方法有以下三種。
- 作用在類的普通方法(非靜態方法)上,鎖的是當前對象實例。
public synchronized void lockInstance() {
System.out.println("鎖的是當前對象實例");
}
- 作用在類的靜態方法上,鎖的是當前類class。
public synchronized static void lockClass() {
System.out.println("鎖的是當前類class");
}
- 作用在代碼塊上,鎖的是指定的對象實例。
public void lockObject(Object obj) {
synchronized (obj) {
System.out.println("鎖的是指定的對象實例obj");
}
}
原理分析
通過以上的用法,我們可以看到synchronized使用起來很簡單,那它究竟是怎麼做到線程間互斥訪問的呢,底層原理及實現是怎樣的呢,接下來我們一一解答。
前一篇文章寫了java對象的記憶體佈局,裡面有一個關於對象頭Markword存儲的內容表格,在synchronized鎖的使用過程中就用到了,如下圖所示。
鎖的狀態
在jdk1.5版本(包含)之前,鎖的狀態只有兩種狀態:“無鎖狀態”和“重量級鎖狀態”,只要有線程訪問共用資源對象,則鎖直接成為重量級鎖,jdk1.6版本後,對synchronized鎖進行了優化,新加了“偏向鎖”和“輕量級鎖”,用來減少上下文的切換以提高性能,所以鎖就有了4種狀態。
- 無鎖
對於共用資源,不涉及多線程的競爭訪問。
- 偏向鎖
共用資源首次被訪問時,JVM會對該共用資源對象做一些設置,比如將對象頭中是否偏向鎖標誌位置為1,對象頭中的線程ID設置為當前線程ID(註意:這裡是操作系統的線程ID),後續當前線程再次訪問這個共用資源時,會根據偏向鎖標識跟線程ID進行比對是否相同,比對成功則直接獲取到鎖,進入臨界區域(就是被鎖保護,線程間只能串列訪問的代碼),這也是synchronized鎖的可重入功能。
- 輕量級鎖
當多個線程同時申請共用資源鎖的訪問時,這就產生了競爭,JVM會先嘗試使用輕量級鎖,以CAS方式來獲取鎖(一般就是自旋加鎖,不阻塞線程採用迴圈等待的方式),成功則獲取到鎖,狀態為輕量級鎖,失敗(達到一定的自旋次數還未成功)則鎖升級到重量級鎖。
- 重量級鎖
如果共用資源鎖已經被某個線程持有,此時是偏向鎖狀態,未釋放鎖前,再有其他線程來競爭時,則會升級到重量級鎖,另外輕量級鎖狀態多線程競爭鎖時,也會升級到重量級鎖,重量級鎖由操作系統來實現,所以性能消耗相對較高。
這4種級別的鎖,在獲取時性能消耗:重量級鎖 > 輕量級鎖 > 偏向鎖 > 無鎖。
鎖升級
鎖升級是針對於synchronized鎖在不同競爭條件下的一種優化,根據鎖在多線程中競爭的程度和狀態,synchronized鎖可在無鎖、偏向鎖、輕量級鎖和重量級鎖之間進行流轉,以降低獲取鎖的成本,提高獲取鎖的性能。
通過下麵這個命令,可以看到所有JVM參數的預設值。
java -XX:+PrintFlagsFinal -version
鎖升級過程
- 當JVM啟動後,一個共用資源對象直到有線程第一個訪問時,這段時間內是處於無鎖狀態,對象頭的Markword里偏向鎖標識位是0,鎖標識位是01。
- 從jdk1.6之後,JVM有兩個預設參數是開啟的,-XX:+UseBiasedLocking(表示啟用偏向鎖,想要關閉偏向鎖,可添加JVM參數:-XX:-UseBiasedLocking),-XX:BiasedLockingStartupDelay=4000(表示JVM啟動4秒後打開偏向鎖,也可以自定義這個延遲時間,如果設置成0,那麼JVM啟動就打開偏向鎖)。
當一個共用資源首次被某個線程訪問時,鎖就會從無鎖狀態升級到偏向鎖狀態,偏向鎖會在Markword的偏向線程ID里存儲當前線程的操作系統線程ID,偏向鎖標識位是1,鎖標識位是01。此後如果當前線程再次進入臨界區域時,只比較這個偏向線程ID即可,這種情況是在只有一個線程訪問的情況下,不再需要操作系統的重量級鎖來切換上下文,提供程式的訪問效率。
另外需要註意的是,由於硬體資源的不斷升級,獲取鎖的成本隨之下降,jdk15版本後預設關閉了偏向鎖。
如果未開啟偏向鎖(或者在JVM偏向鎖延遲時間之前)有線程訪問共用資源則直接由無鎖升級為輕量級鎖,請看第3步。
- 如果未開啟偏向鎖(或者在JVM偏向鎖延遲時間之前),有線程訪問共用資源則直接由無鎖升級為輕量級鎖,開啟偏向線程鎖後,並且當前共用資源鎖已經是偏向鎖時,再有第二個線程訪問共用資源鎖時,此時鎖可能升級為輕量級鎖,也可能還是偏向鎖狀態,因為這取決於線程間的競爭情況,如有沒有競爭,那麼偏向鎖的效率更高(因為頻繁的鎖競爭會導致偏向鎖的撤銷和升級到輕量級鎖),繼續保持偏向鎖。如果有競爭,則鎖狀態會從偏向鎖升級到輕量級鎖,這種情況下輕量級鎖效率會更高。
當第二個線程嘗試獲取偏向鎖失敗時,偏向鎖會升級為輕量級鎖,此時,JVM會使用CAS自旋操作來嘗試獲取鎖,如果成功則進入臨界區域,否則升級為重量級鎖。
輕量級鎖是在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,嘗試拷貝鎖對象頭的Markword到棧幀的Lock Record,若拷貝成功,JVM將使用CAS操作嘗試將對象頭的Markword更新為指向Lock Record的指針,並將Lock Record里的owner指針指向對象頭的Markword。若拷貝失敗,若當前只有一個等待線程,則可通過自旋繼續嘗試, 當自旋超過一定的次數,或者一個線程在持有鎖,一個線程在自旋,又有第三個線程來訪問時,輕量級鎖就會膨脹為重量級鎖。
- 當輕量級鎖獲取鎖失敗時,說明有競爭存在,輕量級鎖會升級為重量級鎖,此時,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>
無鎖 --> 輕量級鎖
無鎖升級到輕量級鎖有兩種情況
- 第一種,關閉偏向鎖,執行時增加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,是無鎖狀態,說明輕量級鎖在執行完同步代碼塊後進行了鎖的釋放。
- 第二種,預設情況下,在偏向鎖延遲時間之前獲取鎖
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鎖原理、優化、使用遠不止本文說的這麼多,感興趣的可進一步探索。