synchronized鎖詳解

2022-09-20 06:04:08

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 synchronized void method(){
///....
}
}

靜態方法 類物件

public class TestCode {
//靜態方法,鎖住的是類物件
public static synchronized void method1(){
///....
}

}

程式碼塊 範例物件 類的範例物件

public class TestCode {
public void method2(){
//同步程式碼塊,鎖住的是該類的範例物件,不能用於靜態方法中
synchronized(this){
//...
}

}

}

Class物件 類物件 //同步程式碼塊,鎖住的是該類的類物件,這種用於靜態或者非靜態方法中都可以
synchronized(TestCode.class){
//...
}

任意是範例象Object 範例物件Object

//同步程式碼塊,鎖住的設定的範例物件
//String物件作為鎖
String lock = "";
synchronized (lock){
//....
}

  

  解決之前的共用問題

    程式碼展示

public static synchronized void increment() { counter++; } //臨界區

public static synchronized void decrement() { counter--; } //臨界區

 

    圖解說明

      synchronized 實際是用物件鎖保證了臨界區內程式碼的原子性:

 

 

 

synchronized底層原理

  synchronized說明

    1. synchronized是JVM內建鎖,基於Monitor機制實現,依賴底層作業系統的互斥原語Mutex(互斥量),它是一個重量級鎖,效能較低。當然,JVM內建鎖在1.5之後版本做了重大的優化,如鎖粗化(Lock Coarsening)鎖消除(Lock Elimination)輕量級鎖(Lightweight Locking)偏向鎖(Biased Locking)自適應自旋(Adaptive Spinning)等技術來減少鎖操作的開銷,內建鎖的並行效能已經基本與Lock持平。 
    2. Java虛擬機器器通過一個同步結構支援方法和方法中的指令序列的同步:monitor。
    3. 同步方法是通過方法中的access_flags中設定ACC_SYNCHRONIZED標誌來實現;同步程式碼塊是通過monitorentermonitorexit來實現。兩個指令的執行是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 的結構也變為輕量級鎖的結構。輕量級鎖所適應的場景是執行緒交替執行同步塊的場合,如果存在同一時間多個執行緒存取同一把鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。

            彙總:

          1. 偏向鎖適用於單個執行緒,以偏向標記表明已經加鎖(本質上沒有鎖,只是標記),加上記錄了偏向的執行緒(作用是認為該執行緒下次還會執行加鎖中的邏輯,這樣下次還加鎖的話,就不需要怎麼改動。如果下次不是該執行緒,則會進行重偏向,有種使用記憶體快取的思維,覺得這次用了下次還會用,乾脆記錄下來,下次省事點的感覺
          2. 輕量級鎖適用於執行緒交替執行同步塊的場合(指競爭不大,如A執行緒持有偏向標記,B來獲取鎖,獲取不到偏向標記,改用CAS方式加鎖這個過程需要一點點時間,這個時間是為了讓A執行緒能剛好完成任務,那麼B執行緒就能加鎖成功獲得輕量級鎖,否則就會將鎖變為重量級鎖,讓A持有直到釋放,然後B再獲取】)
          3. 重量級鎖作用於競爭很大的場景,一般是持有物件持有鎖時間很長導致其他需要鎖的執行緒被迫要進入等待的場景,或者是很多執行緒同時搶鎖,一部分執行緒拿不到鎖不得不等待的場景

        (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)機制是為了解決:在明顯多執行緒競爭劇烈的場景下使用偏向鎖是不合適的

 

    總結

    1. 批次重偏向和批次復原是針對類的優化,和物件無關。
    2. 偏向鎖重偏向一次之後不可再次重偏向。
    3. 當某個類已經觸發批次復原機制後,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(明顯有則效能上的提升)。