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 )【多個執行緒在臨界區內執行,由於程式碼的執行序列不同而導致結果無法預測,稱之為發生了競態條件】
為了避免臨界區的競態條件發生,有多種手段可以達到目的:
注意:
雖然 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的位元組碼指令序列
程式碼展示
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)結構說明
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的結構)
當物件處於可偏向(也就是執行緒ID為0)和已偏向的狀態下,呼叫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 的結構也變為輕量級鎖的結構。輕量級鎖所適應的場景是執行緒交替執行同步塊的場合,如果存在同一時間多個執行緒存取同一把鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
彙總:
(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檢測到有一連串零碎的操作都是對同一物件的加鎖,將會擴大加鎖同步的範圍(即鎖粗化)到整個操作序列的外部。
範例說明
程式碼展示
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(明顯有則效能上的提升)。