背景
在多執行緒編程中,執行緒同步是一個關鍵的概念,它確保了多個執行緒對共享資源的安全訪問,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鎖原理、優化、使用遠不止本文說的這么多,感興趣的可進一步探索,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/557009.html
標籤:其他
下一篇:返回列表