synchronized鎖詳解
synchronized的意義
解決了Java共享記憶體模型帶來的執行緒安全問題:
如:兩個執行緒對初始值為 0 的靜態變數一個做自增,一個做自減,各做 5000 次,結果是 0 嗎?(針對這個問題進行分析)
程式碼展示
public class SyncDemo { private static volatile int counter = 0; //臨界資源 public static void increment() { counter++; } //臨界區 public static void decrement() { counter--; } //臨界區 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { increment(); } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { decrement(); } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); //思考: counter=? log.info("counter={}", counter); } }
問題分析
結論:以上的結果可能是正數、負數、零。為什麼呢?因為 Java 中對靜態變數的自增,自減並不是原子操作。
探究原因
我們可以查看 i++和 i–(i 為靜態變數)的 JVM 位元組碼指令 ( 可以在idea中安裝一個jclasslib插件)
i++的JVM 位元組碼指令
getstatic i // 獲取靜態變數i的值 iconst_1 // 將int常量1壓入操作數棧 iadd // 自增 putstatic i // 將修改後的值存入靜態變數i
i–的JVM 位元組碼指令
getstatic i // 獲取靜態變數i的值 iconst_1 // 將int常量1壓入操作數棧 isub // 自減 putstatic i // 將修改後的值存入靜態變數i。
問題深入:
要知道,我們所寫的程式由程式碼組成,而程式碼會被編譯為指令,每一段可能會有多個指令組成
我們寫了一個 Java 程式,包含一系列的語句,我們會默認期望這些語句的實際運行順序和寫的程式碼順序一致。
但實際上,編譯器、JVM 或者 CPU 都有可能出於優化等目的,對於實際指令執行的順序進行調整,這就是指令重排序。(重排序的好處:提高處理速度)
注意點:不過重排序並不意味著可以任意排序,它需要需要保證重排序後,不改變單執行緒內的語義,否則如果能任意排序的話,程式早就邏輯混亂了
故,如果是單執行緒以上 8 行程式碼是順序執行(不會交錯)沒有問題。但多執行緒下這 8 行程式碼可能交錯運行:

涉及到的概念說明:
臨界區( Critical Section)【一段程式碼塊內如果存在對共享資源的多執行緒讀寫操作,稱這段程式碼塊為臨界區,其共享資源為臨界資源】
-
-
-
- 一個程式運行多個執行緒本身是沒有問題的
- 問題出在多個執行緒訪問共享資源
- 多個執行緒讀共享資源其實也沒有問題
- 在多個執行緒對共享資源讀寫操作時發生指令交錯,就會出現問題
-
-
競態條件( Race Condition )【多個執行緒在臨界區內執行,由於程式碼的執行序列不同而導致結果無法預測,稱之為發生了競態條件】
為了避免臨界區的競態條件發生,有多種手段可以達到目的:
- 阻塞式的解決方案:synchronized,Lock
- 非阻塞式的解決方案:原子變數
注意:
雖然 java 中互斥和同步都可以採用 synchronized 關鍵字來完成,但它們還是有區別的:
-
-
-
-
- 互斥是保證臨界區的競態條件發生,同一時刻只能有一個執行緒執行臨界區程式碼
- 同步是由於執行緒執行的先後、順序不同、需要一個執行緒等待其它執行緒運行到某個點
-
-
-
synchronized的使用
說明
synchronized 同步塊是 Java 提供的一種原子性內置鎖,,Java 中的每個對象都可以把它當作 一個同步鎖來使用,這些 Java 內置的使用者看不到的鎖被稱為內置鎖,也叫作監視器鎖。
加鎖方式展示
| 分類 | 具體分類 | 被鎖對象 | 偽程式碼展示 |
| 方法 | 實例方法 | 類的實例對象 |
public class TestCode { |
| 靜態方法 | 類對象 |
public class TestCode { } |
|
| 程式碼塊 | 實例對象 | 類的實例對象 |
public class TestCode { } } |
| Class對象 | 類對象 | //同步程式碼塊,鎖住的是該類的類對象,這種用於靜態或者非靜態方法中都可以 synchronized(TestCode.class){ //… } |
|
| 任意是實例象Object | 實例對象Object |
//同步程式碼塊,鎖住的配置的實例對象 |
解決之前的共享問題
程式碼展示
public static synchronized void increment() { counter++; } //臨界區 public static synchronized void decrement() { counter--; } //臨界區
圖解說明
synchronized 實際是用對象鎖保證了臨界區內程式碼的原子性:

synchronized底層原理
synchronized說明
-
- synchronized是JVM內置鎖,基於Monitor機制實現,依賴底層作業系統的互斥原語Mutex(互斥量),它是一個重量級鎖,性能較低。當然,JVM內置鎖在1.5之後版本做了重大的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、自適應自旋(Adaptive Spinning)等技術來減少鎖操作的開銷,內置鎖的並發性能已經基本與Lock持平。
- Java虛擬機通過一個同步結構支援方法和方法中的指令序列的同步:monitor。
- 同步方法是通過方法中的access_flags中設置ACC_SYNCHRONIZED標誌來實現;同步程式碼塊是通過monitorenter和monitorexit來實現。兩個指令的執行是JVM通過調用作業系統的互斥原語mutex來實現,被阻塞的執行緒會被掛起、等待重新調度,會導致「用戶態和內核態」兩個態之間來回切換,對性能有較大影響。
查看synchronized的位元組碼指令序列
程式碼展示
private static String lock = ""; public static void increment() { synchronized (lock){ counter++; } } public static synchronized void decrement() { counter--; }
程式碼說明(在idea中安裝一個jclasslib插件,用於分析位元組碼)
針對decrement()方法分析,圖示

說明
圖一展示了 i–的JVM 位元組碼指令,沒有明顯的加鎖現象
圖二展示了方法的訪問標誌(access_flags)為 0x0029,這個是怎麼得來的,結合圖三的訪問標誌表可得
public+ static+ synchronized = 0x0001 +0x0008+ 0x0020 = 0x0029
針對increment()方法分析,圖示

說明
圖一的展示,充分說明了如果synchronized關鍵字載入方法上面則是通過訪問標誌來設置鎖的,位元組碼不怎麼改動
圖二展示了,在方法內部程式碼加鎖在位元組碼層面上是通過(monitorenter+monitorexit 來進行實現的)
至於為什麼會有兩個monitorexit ,一個是正常退出,一個是異常退出。與我們程式碼中加鎖解鎖的方式很類似(拋出異常,然後最終環節還是要解鎖的):
lock.lock(); try{ counter++; }finally { lock.unlock(); }
拓展部分(此外它還有一個手動的API),手動加鎖與解鎖,不過已經被廢棄了【雖然說還可以用,但指不定哪天就沒了】
UnsafeUtils.getUnsafe().monitorEnter(lock); try{ counter++; }finally { UnsafeUtils.getUnsafe().monitorExit(lock); }
Java語言的內置管程synchronized詳解(具體可看 Monitor(管程/監視器)詳解)
Java 參考了 MESA 模型,語言內置的管程(synchronized)對 MESA 模型進行了精簡。MESA模型中,條件變數可以有多個,Java 語言內置的管程里只有一個條件變數。模型如下圖所示:

問題:synchronized加鎖加在對象上,鎖對象是如何記錄鎖狀態的?
首先要了解對象的記憶體布局(可查看 對象的記憶體布局解析)
其次,鎖狀態被記錄在每個對象的對象頭的Mark Word中。
1.Mark Word是如何記錄鎖狀態的
(1)Hotspot通過markOop類型實現Mark Word,具體實現位於markOop.hpp文件中。由於對象需要存儲的運行時數據很多,考慮到虛擬機的記憶體使用,markOop被設計成一個非固定的數據結構,以便在極小的空間存儲盡量多的數據,根據對象的狀態復用自己的存儲空間。
(2)簡單點理解就是:MarkWord 結構搞得這麼複雜,是因為需要節省記憶體,讓同一個記憶體區域在不同階段有不同的用處。
2.Mark Word的結構
(1)32位JVM下的對象結構描述

(2)64位JVM下的對象結構描述

(3)結構說明
-
-
-
-
- hash: 保存對象的哈希碼。運行期間調用System.identityHashCode()來計算,延遲計算,並把結果賦值到這裡。
- age: 保存對象的分代年齡。表示對象被GC的次數,當該次數到達閾值的時候,對象就會轉移到老年代。
- biased_lock: 偏向鎖標識位。由於無鎖和偏向鎖的鎖標識都是 01,沒辦法區分,這裡引入一位的偏向鎖標識位。
- lock: 鎖狀態標識位。區分鎖狀態,比如11時表示對象待GC回收狀態, 只有最後2位鎖標識(11)有效。
- JavaThread*: 保存持有偏向鎖的執行緒ID。偏向模式的時候,當某個執行緒持有對象的時候,對象這裡就會被置為該執行緒的ID。 在後面的操作中,就無需再進行嘗試獲取鎖的動作。這個執行緒ID並不是JVM分配的執行緒ID號,和Java Thread中的ID是兩個概念。
- epoch: 保存偏向時間戳。偏向鎖在CAS鎖操作過程中,偏向性標識,表示對象更偏向哪個鎖。
-
-
-
3.Mark Word中鎖標記枚舉
程式碼展示
enum { locked_value = 0, //00 輕量級鎖 unlocked_value = 1, //001 無鎖 monitor_value = 2, //10 監視器鎖,也叫膨脹鎖,也叫重量級鎖 marked_value = 3, //11 GC標記 biased_lock_pattern = 5 //101 偏向鎖 };
更直觀的理解方式:

跟蹤鎖標記變化(驗證理論)
偏向鎖
(1)概念說明
1.偏向鎖是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,因此為了消除數據在無競爭情況下鎖重入(CAS操作)的開銷而引入偏向鎖。對於沒有鎖競爭的場合,偏向鎖有很好的優化效果。
2.當JVM啟用了偏向鎖模式(jdk6默認開啟),新創建對象的Mark Word中的Thread Id為0,說明此時處於可偏向但未偏向任何執行緒,也叫做匿名偏向狀態(anonymously biased)。
(2)偏向鎖延遲偏向
1.偏向鎖模式存在偏向鎖延遲機制:
HotSpot 虛擬機在啟動後有個 4s 的延遲才會對每個新建的對象開啟偏向鎖模式。
JVM啟動時會進行一系列的複雜活動,比如裝載配置,系統類初始化等等。
在這個過程中會使用大量synchronized關鍵字對對象加鎖,且這些鎖大多數都不是偏向鎖。
為了減少初始化時間,JVM默認延時載入偏向鎖。
2.偏向鎖的JVM指令:
//關閉延遲開啟偏向鎖 ‐XX:BiasedLockingStartupDelay=0 //禁止偏向鎖 ‐XX:‐UseBiasedLocking //啟用偏向鎖 ‐XX:+UseBiasedLocking
3.驗證偏向鎖的延遲機制:
程式碼展示
public class LockEscalationDemo { public static void main(String[] args) throws InterruptedException { log.debug(ClassLayout.parseInstance(new Object()).toPrintable()); //HotSpot 虛擬機在啟動後有個 4s 的延遲才會對每個新建的對象開啟偏向鎖模式 Thread.sleep(5000); Object obj = new Object(); log.debug(ClassLayout.parseInstance(obj).toPrintable()); } }
結果展示

說明:
明顯的可以看出,創建的對象在一定時間後會開啟由無鎖轉變為偏向鎖的模式。而且這一定的時間可以通過JVM指令設置。
(3)偏向鎖狀態跟蹤
情況1:對創建的Object對象加鎖
程式碼展示
public class ObjectTest { public static void main(String[] args) throws InterruptedException { //jvm延遲偏向 Thread.sleep(5000); Object obj = new Test(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); new Thread(()->{ synchronized (obj){ System.out.println(Thread.currentThread().getName()+"加鎖\n"+ClassLayout.parseInstance(obj).toPrintable()); } System.out.println(Thread.currentThread().getName()+"釋放鎖\n"+ClassLayout.parseInstance(obj).toPrintable()); },"Thread1").start(); } }
結果展示

情況2:對類對象進行加鎖(這種一般指在開啟偏向鎖模式前,就已經創建的類對象,否則就是第一種情況了,畢竟4秒的延遲)
程式碼展示(這裡面的延遲偏向時間其實不是必要的,只是為了效果,因為類在jvm啟動的時候就會去載入產生類對象,故沒有偏向)
public class ObjectTest { public static void main(String[] args) throws InterruptedException { //jvm延遲偏向 Thread.sleep(5000); System.out.println(ClassLayout.parseInstance(ObjectTest.class).toPrintable()); new Thread(()->{ synchronized (ObjectTest.class){ System.out.println(Thread.currentThread().getName()+"加鎖\n"+ClassLayout.parseInstance(ObjectTest.class).toPrintable()); } System.out.println(Thread.currentThread().getName()+"釋放鎖\n"+ClassLayout.parseInstance(ObjectTest.class).toPrintable()); },"Thread1").start(); } }
結果展示

(4)偏向鎖撤銷【偏向鎖的撤銷要到GC的安全點(因為程式碼是由指令構成的,所謂安全點就是指某段程式碼被完整執行,如i++,由四條指令構成,如果只執行了兩條,這就是不安全)】
偏向鎖撤銷之調用對象HashCode
1.造成鎖撤銷的說明
調用鎖對象的obj.hashCode()或System.identityHashCode(obj)方法會導致該對象的偏向鎖被撤銷。
因為對於一個對象,其HashCode只會生成一次並保存,偏向鎖是沒有地方保存hashcode的。(可往上看 Mark Word的結構)
-
-
-
-
-
-
-
- 輕量級鎖會在鎖記錄中記錄 hashCode
- 重量級鎖會在 Monitor 中記錄 hashCode
-
-
-
-
-
-
當對象處於可偏向(也就是執行緒ID為0)和已偏向的狀態下,調用HashCode計算將會使對象再也無法偏向:
-
-
-
-
-
-
-
- 當對象可偏向時,MarkWord將變成未鎖定狀態,並只能升級成輕量鎖;
- 當對象正處於偏向鎖時,調用HashCode將使偏向鎖強制升級成重量鎖。
-
-
-
-
-
-
2.驗證說明
當對象正處於偏向鎖時,調用HashCode將使偏向鎖強制升級成重量鎖:
程式碼展示
public static void main(String[] args) throws InterruptedException { //jvm延遲偏向 Thread.sleep(5000); Object obj = new Test(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); new Thread(()->{ synchronized (obj){ System.out.println(Thread.currentThread().getName()+"加鎖中,hashCode前\n"+ClassLayout.parseInstance(obj).toPrintable()); obj.hashCode(); System.out.println(Thread.currentThread().getName()+"加鎖中,hashCode後\n"+ClassLayout.parseInstance(obj).toPrintable()); } System.out.println(Thread.currentThread().getName()+"釋放鎖\n"+ClassLayout.parseInstance(obj).toPrintable()); },"Thread1").start(); }
結果展示

當對象可偏向時,MarkWord將變成未鎖定狀態,並只能升級成輕量鎖:
程式碼展示
public class ObjectTest { public static void main(String[] args) throws InterruptedException { //jvm延遲偏向 Thread.sleep(5000); Object obj = new Test(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); obj.hashCode(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); new Thread(()->{ synchronized (obj){ System.out.println(Thread.currentThread().getName()+"加鎖\n"+ClassLayout.parseInstance(obj).toPrintable()); } System.out.println(Thread.currentThread().getName()+"釋放鎖\n"+ClassLayout.parseInstance(obj).toPrintable()); },"Thread1").start(); } }
結果展示

偏向鎖撤銷之調用wait/notify(這個就自行去驗證吧)
結論:偏向鎖狀態執行obj.notify() 會升級為輕量級鎖,調用obj.wait(timeout) 會升級為重量級鎖。
輕量級鎖
(1)概念說明
倘若偏向鎖失敗,虛擬機並不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段,此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖所適應的場景是執行緒交替執行同步塊的場合,如果存在同一時間多個執行緒訪問同一把鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
匯總:
-
-
-
-
- 偏向鎖適用於單個執行緒,以偏向標記表明已經加鎖(本質上沒有鎖,只是標記),加上記錄了偏向的執行緒(作用是認為該執行緒下次還會執行加鎖中的邏輯,這樣下次還加鎖的話,就不需要怎麼改動。如果下次不是該執行緒,則會進行重偏向,有種使用記憶體快取的思維,覺得這次用了下次還會用,乾脆記錄下來,下次省事點的感覺)
- 輕量級鎖適用於執行緒交替執行同步塊的場合(指競爭不大,如A執行緒持有偏向標記,B來獲取鎖,獲取不到偏向標記,改用CAS方式加鎖【這個過程需要一點點時間,這個時間是為了讓A執行緒能剛好完成任務,那麼B執行緒就能加鎖成功獲得輕量級鎖,否則就會將鎖變為重量級鎖,讓A持有直到釋放,然後B再獲取】)
- 重量級鎖作用於競爭很大的場景,一般是持有對象持有鎖時間很長導致其他需要鎖的執行緒被迫要進入等待的場景,或者是很多執行緒同時搶鎖,一部分執行緒拿不到鎖不得不等待的場景。
- 偏向鎖適用於單個執行緒,以偏向標記表明已經加鎖(本質上沒有鎖,只是標記),加上記錄了偏向的執行緒(作用是認為該執行緒下次還會執行加鎖中的邏輯,這樣下次還加鎖的話,就不需要怎麼改動。如果下次不是該執行緒,則會進行重偏向,有種使用記憶體快取的思維,覺得這次用了下次還會用,乾脆記錄下來,下次省事點的感覺)
-
-
-
(2)疑難問題
1.在鎖變化中容易存在哪些誤區?(後面有源碼分析,大家可以對照一下)
(1)無鎖–》偏向鎖–》輕量級鎖–》重量級鎖
其實不存在無鎖–》偏向鎖 這一步。其次,無鎖和偏向鎖,都是JVM創建的時候給予打上標記的,故算是並列對等的。
而且無鎖和偏向鎖升級到重量級,必然會經過輕量級鎖的程式碼流程。
再者偏向鎖和輕量級鎖都屬於用戶態的加鎖,而重量級鎖屬於內核態部分,所以才會比較耗性能,也就往往希望加鎖能在用戶態。(這也是ReentrantLock更加常用的原因)
(2)輕量級鎖自旋獲取鎖失敗,會膨脹升級為重量級鎖
首先看過源碼的都知道,輕量級鎖是不存在自旋的,在那段程式碼裡面有的只是進行了一次CAS加鎖操作。
其次是,自旋是在生成Monitor對象的過程中,因為這個對象的生成過程較為複雜,所以這個過程一有空閑就會嘗試去進行CAS加鎖,如果能夠成功則加上輕量級鎖,Monitor對象也就不必生成了。因為Monitor對象生成後鎖對象會被塞入其中,這樣鎖對象的標記就會變為重量級鎖標記了。
(3)重量級鎖不存在自旋(如上所說,重量級鎖的自旋式在生成Monitor對象的過程中)
2.有時候會發現重偏向的時候,執行緒ID沒有改變的原因?
原因是JVM層面做了優化,將作業系統的執行緒直接賦予到新的執行緒上面(避免作業系統先銷毀,再去新建)
JVM層面建立的執行緒是依託於作業系統建立的執行緒,JVM的執行緒指向作業系統的執行緒,去做一些操作,如果JVM層面的執行緒要銷毀了,同時又需要建立一些執行緒,這時候作業系統不一定會將老舊的執行緒進行銷毀,而是分配給新JVM層面的執行緒。(詳情可查看 深入理解Java執行緒 )
3.輕量級鎖是否可以降級為偏向鎖?
不可以,不管輕量級鎖還是重量級鎖,釋放鎖後,都是會變成無鎖狀態,因為偏向狀態被撤銷了。
鎖升級場景
情況1,偏向鎖升級為輕量級鎖:
程式碼示例
public static void main(String[] args) throws InterruptedException { //HotSpot 虛擬機在啟動後有個 4s 的延遲才會對每個新建的對象開啟偏向鎖模式 Thread.sleep(5000); Object obj = new Object(); new Thread(new Runnable() { @Override public void run() { log.debug(Thread.currentThread().getName()+"開始執行。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } },"thread1").start(); //控制執行緒競爭時機 Thread.sleep(1); new Thread(new Runnable() { @Override public void run() { log.debug(Thread.currentThread().getName()+"開始執行。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } },"thread2").start(); }
結果展示

結果說明
執行緒1明顯經歷了偏向鎖的加鎖到釋放鎖的步驟,而執行緒2在加鎖前,可以看到對象鎖還在偏向狀態中,此時加鎖,偏向鎖狀態未能重偏向,進入到了CAS的輕量級鎖的加鎖步驟,而且加鎖成功。釋放後變回無鎖狀態。
情況2,輕量級鎖升級為重量級鎖:
程式碼示例
public static void main(String[] args) throws InterruptedException { //HotSpot 虛擬機在啟動後有個 4s 的延遲才會對每個新建的對象開啟偏向鎖模式 Thread.sleep(5000); Object obj = new Object(); new Thread(new Runnable() { @Override public void run() { log.debug(Thread.currentThread().getName()+"開始執行。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } },"thread1").start(); //控制執行緒競爭時機 Thread.sleep(1); new Thread(new Runnable() { @Override public void run() { log.debug(Thread.currentThread().getName()+"開始執行。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } },"thread2").start(); new Thread(new Runnable() { @Override public void run() { log.debug(Thread.currentThread().getName()+"開始執行。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n" +ClassLayout.parseInstance(obj).toPrintable()); } },"thread3").start(); }
結果說明
其實這一步比較簡單就是,當鎖對象已經是輕量鎖了,這時候再遇到競爭的情況。
情況3,偏向鎖升級為重量級鎖:
程式碼示例
public static void main(String[] args) throws InterruptedException { //jvm延遲偏向 Thread.sleep(5000); Object obj = new Test(); new Thread(()->{ System.out.println(Thread.currentThread().getName()+"加鎖前\n"+ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ System.out.println(Thread.currentThread().getName()+"加鎖中1\n"+ClassLayout.parseInstance(obj).toPrintable()); try { Thread.sleep(20000); //模擬業務時間 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"加鎖中2\n"+ClassLayout.parseInstance(obj).toPrintable()); } System.out.println(Thread.currentThread().getName()+"釋放鎖\n"+ClassLayout.parseInstance(obj).toPrintable()); },"Thread1").start(); Thread.sleep(8000); //模擬時間間隔 new Thread(()->{ System.out.println(Thread.currentThread().getName()+"加鎖前\n"+ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj){ System.out.println(Thread.currentThread().getName()+"加鎖中\n"+ClassLayout.parseInstance(obj).toPrintable()); } System.out.println(Thread.currentThread().getName()+"釋放鎖\n"+ClassLayout.parseInstance(obj).toPrintable()); },"Thread2").start(); }
結果展示

結果說明
執行緒1,在偏向鎖狀態中執行業務,執行緒2進行加鎖,此時明顯執行緒2會直接阻塞住(明顯是偏向鎖加鎖不行,輕量級鎖加鎖也不成功,直接轉而到重量級鎖步驟,去生成Monitor對象,進入隊列中等待),而執行緒1中的鎖會急速膨脹(被塞到Monitor對象裡面了,成為重量級鎖),然後執行緒1釋放鎖後執行緒2才會獲取鎖進行下一步。
總結:鎖對象狀態轉換,圖示:

鎖升級的原理分析(深入hotspot的源碼查找)
註:看源碼更多的是驗證理論的正確性,說會記住多少其實都是假的,只能說大概有印象,更多的是對自己總結的理論具備支援的論據,而不是沒有底氣的說個大概。
1.下載openjdk的源碼包
2.在目錄openjdk\hotspot\src\share\vm\runtime下找到synchronizer.cpp文件
程式碼展示
偏向鎖的程式碼邏輯:
/* 偏向鎖的獲取由BiasedLocking::revoke_and_rebias方法實現 1、通過markOop mark = obj->mark()獲取對象的markOop數據mark,即對象頭的Mark Word; 2、判斷mark是否為可偏向狀態,即mark的偏向鎖標誌位為 1,鎖標誌位為 01; 3、判斷mark中JavaThread的狀態:如果為空,則進入步驟(4); 如果指向當前執行緒,則執行同步程式碼塊;如果指向其它執行緒,進入步驟(5); 4、通過CAS原子指令設置mark中JavaThread為當前執行緒ID,如果執行CAS成功, 則執行同步程式碼塊,否則進入步驟(5); 5、如果執行CAS失敗,表示當前存在多個執行緒競爭鎖,當達到全局安全點(safepoint), 獲得偏向鎖的執行緒被掛起,撤銷偏向鎖,並升級為輕量級, 升級完成後被阻塞在安全點的執行緒繼續執行同步程式碼塊; 偏向鎖的撤銷由BiasedLocking::revoke_at_safepoint方法實現 1、偏向鎖的撤銷動作必須等待全局安全點; 2、暫停擁有偏向鎖的執行緒,判斷鎖對象是否處於被鎖定狀態; 3、撤銷偏向鎖,恢復到無鎖(標誌位為 01)或輕量級鎖(標誌位為 00)的狀態 jdk1.6之後默認開啟偏向鎖 開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0(默認有延遲,關閉延遲) 關閉偏向鎖 XX:-UseBiasedLocking */ void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) { //判斷是否開啟偏向鎖 if (UseBiasedLocking) { //判斷是否不在全局安全點 if (!SafepointSynchronize::is_at_safepoint()) { //撤銷和重偏向 BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD); //如果是撤銷和重偏向狀態直接返回 if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) { return; } } else { assert(!attempt_rebias, "can not rebias toward VM thread"); // 偏向鎖的撤銷 只有當其它執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖 BiasedLocking::revoke_at_safepoint(obj); } assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } // 獲取輕量級鎖 當關閉偏向鎖功能,或多個執行緒競爭偏向鎖導致偏向鎖升級為輕量級鎖 slow_enter (obj, lock, THREAD) ; }
輕量級鎖的程式碼邏輯:
進入:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { // 獲取對象的markOop數據 mark markOop mark = obj->mark(); assert(!mark->has_bias_pattern(), "should not see bias pattern here"); //判斷mark是否為無鎖狀態:mark的偏向鎖標誌位為 0,鎖標誌位為 01; if (mark->is_neutral()) { //把mark保存到BasicLock對象的_displaced_header欄位 lock->set_displaced_header(mark); //通過CAS嘗試將Mark Word更新為指向BasicLock對象的指針,如果更新成功,表示競爭到鎖,則執行同步程式碼 // Atomic::cmpxchg_ptr原子操作保證只有一個執行緒可以把指向棧幀的指針複製到Mark Word if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } } else //如果當前mark處於加鎖狀態,且mark中的ptr指針指向當前執行緒的棧幀,表示為重入操作,不需要競爭鎖 if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { assert(lock != mark->locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock"); lock->set_displaced_header(NULL); return; } if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) { lock->set_displaced_header (NULL) ; return ; } // 這時候需要膨脹為重量級鎖,膨脹前,設置Displaced Mark Word為一個特殊值,代表該鎖正在用一個重量級鎖的monitor lock->set_displaced_header(markOopDesc::unused_mark()); // 鎖膨脹的過程,該方法返回一個ObjectMonitor對象,然後調用其enter方法 ObjectSynchronizer::inflate(2, obj())->enter(THREAD); }
釋放鎖:
// 輕量級鎖釋放 void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) { fast_exit (object, lock, THREAD) ; } //輕量級鎖的釋放 void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) { assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here"); // 取出棧幀中保存的mark word markOop dhw = lock->displaced_header(); markOop mark ; if (dhw == NULL) { //如果是null,說明是重入的 mark = object->mark() ; assert (!mark->is_neutral(), "invariant") ; if (mark->has_locker() && mark != markOopDesc::INFLATING()) { assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ; } if (mark->has_monitor()) { ObjectMonitor * m = mark->monitor() ; assert(((oop)(m->object()))->mark() == mark, "invariant") ; assert(m->is_entered(THREAD), "invariant") ; } return ; } //獲取當前鎖對象的mark word mark = object->mark() ; if (mark == (markOop) lock) { assert (dhw->is_neutral(), "invariant") ; // 通過CAS嘗試把dhw替換到當前的Mark Word,如果CAS成功,說明成功的釋放了鎖 if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) { TEVENT (fast_exit: release stacklock) ; return; } } // CAS失敗,此時有競爭,開始膨脹 ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ; }
鎖膨脹程式碼邏輯:
//鎖膨脹過程 膨脹完成返回monitor時,並不表示該執行緒競爭到了鎖, //真正的鎖競爭發生在ObjectMonitor::enter方法中 ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) { assert (Universe::verify_in_progress() || !SafepointSynchronize::is_at_safepoint(), "invariant") ; for (;;) { const markOop mark = object->mark() ; assert (!mark->has_bias_pattern(), "invariant") ; //mark是以下狀態中的一種: // * Inflated(重量級鎖狀態) - 直接返回 // * Stack-locked(輕量級鎖狀態) - 膨脹 // * INFLATING(膨脹中) - 忙等待直到膨脹完成 // * Neutral(無鎖狀態) - 膨脹 // * BIASED(偏向鎖) - 非法狀態,在這裡不會出現 // CASE: inflated // 判斷當前是否為重量級鎖狀態,即Mark Word的鎖標識位為 10,如果已經是重量級鎖狀態直接返回 if (mark->has_monitor()) { //獲取指向ObjectMonitor的指針,並返回,膨脹過程已經完成 ObjectMonitor * inf = mark->monitor() ; assert (inf->header()->is_neutral(), "invariant"); assert (inf->object() == object, "invariant") ; assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid"); return inf ; } /* 如果當前鎖處於膨脹中,說明該鎖正在被其它執行緒執行膨脹操作, 則當前執行緒就進行自旋等待鎖膨脹完成,這裡需要注意一點,雖然是自旋操作, 但不會一直佔用cpu資源,每隔一段時間會通過os::NakedYield方法放棄cpu資源, 或通過park方法掛起;如果其他執行緒完成鎖的膨脹操作,則退出自旋並返回 */ if (mark == markOopDesc::INFLATING()) { TEVENT (Inflate: spin while INFLATING) ; // 檢查是否處於膨脹中狀態(其他執行緒正在膨脹中), //如果是膨脹中,就調用ReadStableMark方法進行等待, //ReadStableMark方法執行完畢後再通過continue繼續檢查, //ReadStableMark方法中還會調用os::NakedYield()釋放CPU資源 ReadStableMark(object) ; continue ; } /* 如果當前是輕量級鎖狀態,即鎖標識位為 00 ,開始膨脹 1、通過omAlloc方法,獲取一個可用的ObjectMonitor monitor,並重置monitor數據; 2、通過CAS嘗試將Mark Word設置為markOopDesc:INFLATING,標識當前鎖正在膨脹中, 如果CAS失敗,說明同一時刻其它執行緒已經將Mark Word設置為markOopDesc:INFLATING, 當前執行緒進行自旋等待膨脹完成; 3、如果CAS成功,設置monitor的各個欄位:_header、_owner和_object等,並返回 */ if (mark->has_locker()) { // 當前輕量級鎖狀態,創建ObjectMonitor對象,並初始化 ObjectMonitor * m = omAlloc (Self) ; m->Recycle(); m->_Responsible = NULL ; m->OwnerIsThread = 0 ; m->_recursions = 0 ; m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // Consider: maintain by type/class // 設置狀態為膨脹中 markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ; if (cmp != mark) { //CAS失敗,說明衝突了,自旋等待 omRelease (Self, m, true) ; //釋放monitor continue ; // Interference -- just retry } markOop dmw = mark->displaced_mark_helper() ; assert (dmw->is_neutral(), "invariant") ; //CAS成功,設置ObjectMonitor的_header、_owner和_object等 m->set_header(dmw) ; m->set_owner(mark->locker()); m->set_object(object); // TODO-FIXME: assert BasicLock->dhw != 0. guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ; // 將鎖對象的mark word設置為重量級鎖狀態 object->release_set_mark(markOopDesc::encode(m)); if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ; TEVENT(Inflate: overwrite stacklock) ; if (TraceMonitorInflation) { if (object->is_instance()) { ResourceMark rm; tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s", (void *) object, (intptr_t) object->mark(), object->klass()->external_name()); } } return m ; } // 如果是無鎖狀態 assert (mark->is_neutral(), "invariant"); // 創建ObjectMonitor對象,並初始化 ObjectMonitor * m = omAlloc (Self) ; // prepare m for installation - set monitor to initial state m->Recycle(); m->set_header(mark); m->set_owner(NULL); m->set_object(object); m->OwnerIsThread = 1 ; m->_recursions = 0 ; m->_Responsible = NULL ; m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // consider: keep metastats by type/class // CAS 設置對象頭標誌為重量級鎖 if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) { // 有競爭CAS失敗,釋放monitor重試 m->set_object (NULL) ; m->set_owner (NULL) ; m->OwnerIsThread = 0 ; m->Recycle() ; omRelease (Self, m, true) ; m = NULL ; continue ; } if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ; TEVENT(Inflate: overwrite neutral) ; if (TraceMonitorInflation) { if (object->is_instance()) { ResourceMark rm; tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s", (void *) object, (intptr_t) object->mark(), object->klass()->external_name()); } } return m ; } }
重量級鎖的程式碼邏輯(在文件objectMonitor.cpp裡面):
進入:
// 重量級鎖執行邏輯 void ATTR ObjectMonitor::enter(TRAPS) { Thread * const Self = THREAD ; void * cur ; //通過CAS嘗試把monitor的_owner欄位設置為當前執行緒,直接獲取鎖 cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; if (cur == NULL) { // Either ASSERT _recursions == 0 or explicitly set _recursions = 0. assert (_recursions == 0 , "invariant") ; assert (_owner == Self, "invariant") ; // CONSIDER: set or assert OwnerIsThread == 1 return ; } //設置之前的_owner指向當前執行緒,說明當前執行緒已經持有鎖,此次為重入,_recursions自增 if (cur == Self) { // TODO-FIXME: check for integer overflow! BUGID 6557169. _recursions ++ ; return ; } //當前執行緒是之前持有輕量級鎖的執行緒。由輕量級鎖膨脹且第一次調用enter方法,那cur是指向Lock Record的指針 //設置_recursions為1,_owner為當前執行緒,該執行緒成功獲得鎖並返回 if (Self->is_lock_owned ((address)cur)) { assert (_recursions == 0, "internal state error"); _recursions = 1 ; // Commute owner from a thread-specific on-stack BasicLockObject address to a full-fledged "Thread *". _owner = Self ; OwnerIsThread = 1 ; return ; } // We've encountered genuine contention. assert (Self->_Stalled == 0, "invariant") ; Self->_Stalled = intptr_t(this) ; // 調用系統同步操作之前,先嘗試自旋獲得鎖 if (Knob_SpinEarly && TrySpin (Self) > 0) { assert (_owner == Self , "invariant") ; assert (_recursions == 0 , "invariant") ; assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ; //自旋的過程中獲得了鎖,則直接返回 Self->_Stalled = 0 ; return ; } assert (_owner != Self , "invariant") ; assert (_succ != Self , "invariant") ; assert (Self->is_Java_thread() , "invariant") ; JavaThread * jt = (JavaThread *) Self ; assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ; assert (jt->thread_state() != _thread_blocked , "invariant") ; assert (this->object() != NULL , "invariant") ; assert (_count >= 0, "invariant") ; Atomic::inc_ptr(&_count); EventJavaMonitorEnter event; { // Change java thread status to indicate blocked on monitor enter. JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this); DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt); if (JvmtiExport::should_post_monitor_contended_enter()) { JvmtiExport::post_monitor_contended_enter(jt, this); } OSThreadContendState osts(Self->osthread()); ThreadBlockInVM tbivm(jt); Self->set_current_pending_monitor(this); // TODO-FIXME: change the following for(;;) loop to straight-line code. for (;;) { jt->set_suspend_equivalent(); // monitor競爭失敗的執行緒,等待獲取鎖 // 1. 將當前執行緒封裝為 node 塞到隊列 cxq 的隊頭 // 2. 調用 park 掛起當前執行緒 // 3. 被喚醒後再次嘗試獲取鎖(在喚醒時候會根據不同的喚醒策略定義 cxq 與 EntryList 的優先順序) EnterI (THREAD) ; if (!ExitSuspendEquivalent(jt)) break ; _recursions = 0 ; _succ = NULL ; exit (false, Self) ; jt->java_suspend_self(); } Self->set_current_pending_monitor(NULL); } Atomic::dec_ptr(&_count); assert (_count >= 0, "invariant") ; Self->_Stalled = 0 ; // Must either set _recursions = 0 or ASSERT _recursions == 0. assert (_recursions == 0 , "invariant") ; assert (_owner == Self , "invariant") ; assert (_succ != Self , "invariant") ; assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ; DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt); if (JvmtiExport::should_post_monitor_contended_entered()) { JvmtiExport::post_monitor_contended_entered(jt, this); } if (event.should_commit()) { event.set_klass(((oop)this->object())->klass()); event.set_previousOwner((TYPE_JAVALANGTHREAD)_previous_owner_tid); event.set_address((TYPE_ADDRESS)(uintptr_t)(this->object_addr())); event.commit(); } if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) { ObjectMonitor::_sync_ContendedLockAttempts->inc() ; } }
釋放鎖(這部分程式碼其實有助於作證java的管程模型):
//重量級鎖解鎖邏輯 void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) { Thread * Self = THREAD ; // 如果_owner不是當前執行緒 if (THREAD != _owner) { // 當前執行緒是之前持有輕量級鎖的執行緒。由輕量級鎖膨脹後還沒調用過enter方法,_owner會是指向Lock Record的指針。 if (THREAD->is_lock_owned((address) _owner)) { // 如果owner位於當前執行緒調用棧幀,說明該鎖是輕量級鎖膨脹來的 assert (_recursions == 0, "invariant") ; //修改owner屬性 _owner = THREAD ; _recursions = 0 ; OwnerIsThread = 1 ; } else { // 其他執行緒佔用該鎖,直接返回 TEVENT (Exit - Throw IMSX) ; assert(false, "Non-balanced monitor enter/exit!"); if (false) { THROW(vmSymbols::java_lang_IllegalMonitorStateException()); } return; } } // 如果重入計數器不為0.減1後返回 if (_recursions != 0) { _recursions--; // this is simple recursive enter TEVENT (Inflated exit - recursive) ; return ; } // _Responsible設置為null if ((SyncFlags & 4) == 0) { _Responsible = NULL ; } #if INCLUDE_TRACE if (not_suspended && Tracing::is_event_enabled(TraceJavaMonitorEnterEvent)) { _previous_owner_tid = SharedRuntime::get_java_tid(Self); } #endif for (;;) { assert (THREAD == _owner, "invariant") ; // Knob_ExitPolicy默認為0 if (Knob_ExitPolicy == 0) { // 將_owner屬性置為NULL,釋放鎖,如果某個執行緒正在自旋搶佔該鎖,則會搶佔成功, //這種策略會優先保證通過自旋搶佔鎖的執行緒獲取鎖,而其他處於等待隊列中的執行緒則靠後 OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock //讓修改立即生效 OrderAccess::storeload() ; // See if we need to wake a successor // 如果_EntryList或者cxq都是空的,則直接返回 if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { TEVENT (Inflated exit - simple egress) ; return ; } TEVENT (Inflated exit - complex egress) ; // 如果_EntryList或者cxq不是空的,則通過CAS設置owner屬性為當前執行緒,嘗試搶佔鎖 if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) { //搶佔失敗則返回,等佔用該鎖的執行緒釋放後再處理隊列中的等待執行緒 return ; } TEVENT (Exit - Reacquired) ; } else { if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock OrderAccess::storeload() ; // Ratify the previously observed values. if (_cxq == NULL || _succ != NULL) { TEVENT (Inflated exit - simple egress) ; return ; } //有可能cxq插入了一個新節點,導致上面的if不成立,需要重新獲取鎖 if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) { TEVENT (Inflated exit - reacquired succeeded) ; return ; } TEVENT (Inflated exit - reacquired failed) ; } else { //如果_EntryList或者cxq不是空的則不釋放鎖,避免二次搶佔鎖,即優先處理等待隊列中的執行緒 TEVENT (Inflated exit - complex egress) ; } } guarantee (_owner == THREAD, "invariant") ; ObjectWaiter * w = NULL ; //根據QMode的不同會有不同的喚醒策略,默認為0 int QMode = Knob_QMode ; // 從cxq或EntryList中獲取頭節點, // 通過ObjectMonitor::ExitEpilog方法喚醒該節點封裝的執行緒, // 喚醒操作最終由unpark完成 if (QMode == 2 && _cxq != NULL) { // QMode == 2 : cxq has precedence over EntryList. // Try to directly wake a successor from the cxq. // If successful, the successor will need to unlink itself from cxq. // QMode == 2並且cxq隊列不為空 : cxq中的執行緒有更高優先順序,直接喚醒cxq的隊首執行緒 w = _cxq ; assert (w != NULL, "invariant") ; assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ; //通過unpark喚醒cxq對應的執行緒,喚醒後會將cxq從隊列中移除 ExitEpilog (Self, w) ; return ; } if (QMode == 3 && _cxq != NULL) { // 把_cxq隊首元素放入_EntryList的尾部 w = _cxq ; for (;;) { assert (w != NULL, "Invariant") ; //將_cxq置為NULL,如果失敗則更新w,重新嘗試直到成功為止 //置為NULL後,如果有新的節點插入進來就形成了一個新的cxq隊列 ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ; } assert (w != NULL , "invariant") ; ObjectWaiter * q = NULL ; ObjectWaiter * p ; //遍歷cxq中的所有節點,將其置為TS_ENTER for (p = w ; p != NULL ; p = p->_next) { guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; } ObjectWaiter * Tail ; //遍歷_EntryList找到末尾元素,將w插入到後面 for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ; if (Tail == NULL) { _EntryList = w ; } else { Tail->_next = w ; //將w插入_EntryList隊列尾部 w->_prev = Tail ; } } if (QMode == 4 && _cxq != NULL) { // 把_cxq隊首元素放入_EntryList的頭部 w = _cxq ; for (;;) { assert (w != NULL, "Invariant") ; //將_cxq置為NULL,如果失敗則更新w,重新嘗試直到成功為止 ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ; } assert (w != NULL , "invariant") ; ObjectWaiter * q = NULL ; ObjectWaiter * p ; //遍歷cxq中的所有節點,將其置為TS_ENTER for (p = w ; p != NULL ; p = p->_next) { guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; } // Prepend the RATs to the EntryList //插入到_EntryList的頭部 if (_EntryList != NULL) { q->_next = _EntryList ; _EntryList->_prev = q ; } _EntryList = w ; // Fall thru into code that tries to wake a successor from EntryList } w = _EntryList ; // _EntryList不為空,直接從_EntryList中喚醒執行緒 if (w != NULL) { assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; ////通過unpark喚醒w對應的執行緒,喚醒後會該執行緒會負責將w從EntryList鏈表中移除 ExitEpilog (Self, w) ; return ; } //如果_EntryList為空 w = _cxq ; if (w == NULL) continue ;//如果_cxq和_EntryList隊列都為空,自旋 for (;;) { assert (w != NULL, "Invariant") ; //自旋再獲得cxq首結點 ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ; } TEVENT (Inflated exit - drain cxq into EntryList) ; assert (w != NULL , "invariant") ; assert (_EntryList == NULL , "invariant") ; // cxq不為空,_EntryList為空的情況 if (QMode == 1) { //遍歷cxq中的元素將其加入到_EntryList中,注意順序跟cxq中是反的 ObjectWaiter * s = NULL ; ObjectWaiter * t = w ; ObjectWaiter * u = NULL ; while (t != NULL) { guarantee (t->TState == ObjectWaiter::TS_CXQ, "invariant") ; t->TState = ObjectWaiter::TS_ENTER ; u = t->_next ; t->_prev = u ; t->_next = s ; s = t; t = u ; } _EntryList = s ; //將_cxq中的元素轉移到_EntryList,並反轉順序 assert (s != NULL, "invariant") ; } else { // QMode == 0 or QMode == 2 // 將_cxq中的元素轉移到_EntryList _EntryList = w ; ObjectWaiter * q = NULL ; ObjectWaiter * p ; for (p = w ; p != NULL ; p = p->_next) { guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; } } if (_succ != NULL) continue; w = _EntryList ; if (w != NULL) { guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; // 喚醒_EntryList隊首元素 ExitEpilog (Self, w) ; return ; } } }
3.進行圖解
Synchronized重量級鎖加鎖解鎖執行邏輯:

Synchronized輕量級鎖源碼分析:

synchronized鎖優化
偏向鎖批量重偏向&批量撤銷
概念說明
從偏向鎖的加鎖解鎖過程中可看出,當只有一個執行緒反覆進入同步塊時,偏向鎖帶來的性能開銷基本可以忽略,但是當有其他執行緒嘗試獲得鎖時,就需要等到safe point時,再將偏向鎖撤銷為無鎖狀態或升級為輕量級,會消耗一定的性能,所以在多執行緒競爭頻繁的情況下,偏向鎖不僅不能提高性能,還會導致性能下降。於是,就有了批量重偏向與批量撤銷的機制。
JVM的默認參數值:設置JVM參數-XX:+PrintFlagsFinal,在項目啟動時即可輸出JVM的默認參數值
intx BiasedLockingBulkRebiasThreshold = 20 //默認偏向鎖批量重偏向閾值 intx BiasedLockingBulkRevokeThreshold = 40 //默認偏向鎖批量撤銷閾值
我們可以通過-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 來手動設置閾值
原理
以class為單位,為每個class維護一個偏向鎖撤銷計數器,每一次該class的對象發生偏向撤銷操作時,該計數器+1,當這個值達到重偏向閾值(默認20)時,JVM就認為該class的偏向鎖有問題,因此會進行批量重偏向。
每個class對象會有一個對應的epoch欄位,每個處於偏向鎖狀態對象的Mark Word中也有該欄位,其初始值為創建該對象時class中的epoch的值。每次發生批量重偏向時,就將該值+1,同時遍歷JVM中所有執行緒的棧,找到該class所有正處於加鎖狀態的偏向鎖,將其epoch欄位改為新值。下次獲得鎖時,發現當前對象的epoch值和class的epoch不相等,那就算當前已經偏向了其他執行緒,也不會執行撤銷操作,而是直接通過CAS操作將其Mark Word的Thread Id 改成當前執行緒Id。
當達到重偏向閾值(默認20)後,假設該class計數器繼續增長,當其達到批量撤銷的閾值後(默認40),JVM就認為該class的使用場景存在多執行緒競爭,會標記該class為不可偏向,之後,對於該class的鎖,直接走輕量級鎖的邏輯。(注意:時間-XX:BiasedLockingDecayTime=25000ms範圍內沒有達到40次,撤銷次數清為0,重新計時)
原理驗證
驗證程式碼展示
public class BiasedLockingTest { public static void main(String[] args) throws InterruptedException { //延時產生可偏向對象 Thread.sleep(5000); // 創建一個list,來存放鎖對象 List<Object> list = new ArrayList<>(); // 執行緒1 new Thread(() -> { for (int i = 0; i < 50; i++) { // 新建鎖對象 Object lock = new Object(); synchronized (lock) { list.add(lock); } } try { //為了防止JVM執行緒復用,在創建完對象後,保持執行緒thead1狀態為存活 Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } }, "thead1").start(); //睡眠3s鍾保證執行緒thead1創建對象完成 Thread.sleep(3000); log.debug("列印thead1,list中第20個對象的對象頭:"); log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable())); // 執行緒2 new Thread(() -> { for (int i = 0; i < 40; i++) { Object obj = list.get(i); synchronized (obj) { if(i>=15&&i<=21||i>=38){ log.debug("thread2-第" + (i + 1) + "次加鎖執行中\t"+ ClassLayout.parseInstance(obj).toPrintable()); } } if(i==17||i==19){ log.debug("thread2-第" + (i + 1) + "次釋放鎖\t"+ ClassLayout.parseInstance(obj).toPrintable()); } } try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } }, "thead2").start(); Thread.sleep(3000); new Thread(() -> { for (int i = 0; i < 50; i++) { Object lock =list.get(i); if(i>=17&&i<=21||i>=35&&i<=41){ log.debug("thread3-第" + (i + 1) + "次準備加鎖\t"+ ClassLayout.parseInstance(lock).toPrintable()); } synchronized (lock){ if(i>=17&&i<=21||i>=35&&i<=41){ log.debug("thread3-第" + (i + 1) + "次加鎖執行中\t"+ ClassLayout.parseInstance(lock).toPrintable()); } } } },"thread3").start(); Thread.sleep(3000); log.debug("查看新創建的對象"); log.debug((ClassLayout.parseInstance(new Object()).toPrintable())); LockSupport.park(); } }
驗證批量重偏向
說明
當撤銷偏向鎖閾值超過 20 次後,jvm 會覺得,是不是偏向錯了,於是會在給這些對象加鎖時重新偏向至加鎖執行緒,重偏向會重置對象的Thread ID。
結果分析
thread1: 創建50個偏向執行緒thread1的偏向鎖 1-50 偏向鎖
thread2:
1-18 偏向鎖撤銷,升級為輕量級鎖 (thread1釋放鎖之後為偏向鎖狀態)
19-40 偏向鎖撤銷達到閾值(20),執行了批量重偏向 (測試結果在第19就開始批量重偏向了)
驗證批量撤銷
說明
當撤銷偏向鎖閾值超過 40 次後,jvm 會認為不該偏向,於是整個類的所有對象都會變為不可偏向的,新建的對象也是不可偏向的。
注意:時間-XX:BiasedLockingDecayTime=25000ms範圍內沒有達到40次,撤銷次數清為0, 重新計時。
結果分析
thread3:
1-18 從無鎖狀態直接獲取輕量級鎖 (thread2釋放鎖之後變為無鎖狀態)
19-40 偏向鎖撤銷,升級為輕量級鎖 (thread2釋放鎖之後為偏向鎖狀態)
41-50 達到偏向鎖撤銷的閾值40,批量撤銷偏向鎖,升級為輕量級鎖 (thread1釋放鎖之後為偏向鎖狀態)
新創建的對象: 無鎖狀態
應用場景
批量重偏向(bulk rebias)機制是為了解決:一個執行緒創建了大量對象並執行了初始的同步操作,後來另一個執行緒也來將這些對象作為鎖對象進行操作,這樣會導致大量的偏向鎖撤銷操作。
批量撤銷(bulk revoke)機制是為了解決:在明顯多執行緒競爭劇烈的場景下使用偏向鎖是不合適的。
總結
-
- 批量重偏向和批量撤銷是針對類的優化,和對象無關。
- 偏向鎖重偏向一次之後不可再次重偏向。
- 當某個類已經觸發批量撤銷機制後,JVM會默認當前類產生了嚴重的問題,剝奪了該類的新實例對象使用偏向鎖的權利
自旋優化
概念說明
重量級鎖競爭的時候,還可以使用自旋來進行優化,如果當前執行緒自旋成功(即這時候持鎖執行緒已經退出了同步塊,釋放了鎖),這時當前執行緒就可以避免阻塞。
- 自旋會佔用 CPU 時間,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發揮優勢。
- 在 Java 6 之後自旋是自適應的,比如對象剛剛的一次自旋操作成功過,那麼認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,比較智慧。
- Java 7 之後不能控制是否開啟自旋功能
注意:自旋的目的是為了減少執行緒掛起的次數,盡量避免直接掛起執行緒(掛起操作涉及系統調用,存在用戶態和內核態切換,這才是重量級鎖最大的開銷)。
鎖粗化
概念說明
假設一系列的連續操作都會對同一個對象反覆加鎖及解鎖,甚至加鎖操作是出現在循環體中的,即使沒有出現執行緒競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。如果JVM檢測到有一連串零碎的操作都是對同一對象的加鎖,將會擴大加鎖同步的範圍(即鎖粗化)到整個操作序列的外部。
示例說明
程式碼展示
StringBuffer buffer = new StringBuffer(); /** * 鎖粗化 */ public void append(){ buffer.append("aaa").append(" bbb").append(" ccc"); }
程式碼說明
上述程式碼每次調用 buffer.append 方法都需要加鎖和解鎖,如果JVM檢測到有一連串的對同一個對象加鎖和解鎖的操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。
鎖消除
概念說明
鎖消除即刪除不必要的加鎖操作。鎖消除是Java虛擬機在JIT編譯期間,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過鎖消除,可以節省毫無意義的請求鎖時間。
示例說明
程式碼展示
/** * 鎖消除 * -XX:+EliminateLocks 開啟鎖消除(jdk8默認開啟) * -XX:-EliminateLocks 關閉鎖消除 * @param str1 * @param str2 */ public void append(String str1, String str2) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(str1).append(str2); } public static void main(String[] args) throws InterruptedException { LockEliminationTest demo = new LockEliminationTest(); long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { demo.append("aaa", "bbb"); } long end = System.currentTimeMillis(); System.out.println("執行時間:" + (end - start) + " ms"); }
程式碼說明
StringBuffer的append是個同步方法,但是append方法中的 StringBuffer 屬於一個局部變數,不可能從該方法中逃逸出去,因此其實這過程是執行緒安全的,可以將鎖消除。(涉及到了逃逸分析的概念,可查看 逃逸分析(Escape Analysis)詳解)
這部分程式碼與鎖粗化很相似,但卻不同,StringBuffer定義在方法內是當做局部變數分配到了棧上,每個執行緒都會有自己的棧(JVM的記憶體模型的知識),故是私有的,不存在競爭,可以消除。而定義在方法外,則會分配到堆上,是共享的,所有執行緒都可以使用,故不能消除,但是在編譯的時候檢測到對同一個對象反覆加鎖及解鎖,可以擴大加鎖範圍來達到減少加鎖操作。
測試結果: 關閉鎖消除執行時間4688 ms 開啟鎖消除執行時間:2601 ms(明顯有則性能上的提升)。

