synchronized原理及鎖膨脹


一、對象頭

​ 在HotSpot虛擬機里,對象在堆內存中的存儲布局可以划分為三個部分:對象頭,實例數據和對齊填充,這里我們就先介紹一下對象頭。

​ 在HotSpot虛擬機的對象頭部分包括三類信息:

  • 第一類是用於存儲對象自身的運行時數據,如哈希嗎,GC分代年齡,鎖狀態標志,線程持有的鎖,偏向線程ID,偏向時間軸等,這部分的數據的長度在32位和64位的虛擬機中(未開啟壓縮指針)中分別為32個比特和64個比特。官方稱之為"Mark Word"。
  • 對象頭的另一部分是類型指針,即對象指向它的類型元數據的指針,Java虛擬機通過這個指針來確定該對象是哪個類的實例。(並不是所有的虛擬機實現都必須在對象數據上保留類型指針)
  • 如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因為虛擬機虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是如果數組的長度是不確定的,將無法通過元數據中的信息推斷出數組的大小。

二、對象頭的格式

32位虛擬機

  • 普通對象
|--------------------------------------------------------------|
|					 Object Header (64 bits) 				   |
|------------------------------------|-------------------------| 
| Mark Word (32 bits) 	  			 |  Klass Word (32 bits)   |
|------------------------------------|-------------------------|
  • 數組對象(多一個數組長度)
|---------------------------------------------------------------------------------|
|					 			Object Header (96 bits) 						  |
|--------------------------------|-----------------------|------------------------|
| 		Mark Word(32bits) 		 | Klass Word(32bits) 	 |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

其中對象頭Mark Word(32為虛擬機)的結構為:

|--------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                   |       State        |
|--------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:0 | lock:01 |       Normal       |
|--------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:01 |       Biased       |
|--------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:00 | Lightweight Locked |
|--------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:10 | Heavyweight Locked |
|--------------------------------------------------------|--------------------|
|                                              | lock:11 |    Marked for GC   |
|--------------------------------------------------------|--------------------|

其中各個部分的含義如下:

lock:為2位的鎖狀態標記位由於希望用盡可能少的二進制位表示盡可能多的信息,所以設置了lock標記。該標記的值不同,整個mark word表示的含義不同。

biased_lock lock 狀態
0 01 無鎖
1 01 偏向鎖
0 00 輕量級鎖
0 10 重量級鎖
0 11 GC標記

biased_lock:對象是否啟用偏向鎖標記,只占1個二進制位。為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖。

age:4位的Java對象年齡。在GC中,如果對象在Survivor區復制一次,年齡增加1。當對象達到設定的閾值時,將會晉升到老年代。默認情況下,並行GC的年齡閾值為15,並發GC的年齡閾值為6。由於age只有4位,所以最大值為15,這就是-XX:MaxTenuringThreshold選項最大值為15的原因。

identity_hashcode:25位的對象標識Hash碼,采用延遲加載技術。調用方法System.identityHashCode()計算,並會將結果寫到該對象頭中。當對象被鎖定時,該值會移動到管程Monitor中。

thread:持有偏向鎖的線程ID。
epoch:偏向時間戳。
ptr_to_lock_record:指向棧中鎖記錄的指針。
ptr_to_heavyweight_monitor:指向管程Monitor的指針。

對於64位的虛擬機其Mark Word格式如下:

|--------------------------------------------------------------------|--------------------|
| 					 Mark Word (64 bits) 							 | 		  State 	  |
|--------------------------------------------------------------------|--------------------|
| unused:25    | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | 		  Normal	  |
|--------------------------------------------------------------------|--------------------|
| thread:54    | epoch:2	 | unused:1 | age:4 | biased_lock:1 | 01 | 		  Biased	  |
|--------------------------------------------------------------------|--------------------|
| 					ptr_to_lock_record:62						| 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| 			ptr_to_heavyweight_monitor:62 						| 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| 																| 11 | 	  Marked for GC	  |
|--------------------------------------------------------------------|--------------------|

三、Monitor

3.1、概述

​ Monitor 被翻譯為監視器管程,Monitor 的重要特點是,同一個時刻,只有一個 進程/線程 能進入 monitor 中定義的臨界區,這使得 monitor 能夠達到互斥的效果。每個 Java 對象都可以關聯一個 Monitor 對象,如果使用 synchronized 給對象上鎖(重量級)之后,該對象頭的Mark Word 中就被設置指向 Monitor 對象的指針

3.2、Monitor的結構

ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 后進入 _Owner 區域並把monitor中的owner變量設置為當前線程同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復為null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。

image-20201007173243767

  • 剛開始 Monitor 中 Owner 為 null當 Thread-2 執行 synchronized(obj) 就會將 Monitor 的所有者 Owner 置為 Thread-2,Monitor中只能有一個 Owner

  • 在 Thread-2 上鎖的過程中,如果 Thread-3,Thread-4,Thread-5 也來執行 synchronized(obj),就會進入EntryList BLOCKED(阻塞)

  • Thread-2 執行完同步代碼塊的內容,然后喚醒 EntryList 中等待的線程來競爭鎖,競爭的時是非公平的

  • 圖中 WaitSet 中的 Thread-0,Thread-1 是之前獲得過鎖,但條件不滿足(線程調用 wait() 方法)進入 WAITING 狀態的線程需要被notify喚醒才能再次嘗試獲取鎖。

注意:

  • synchronized 必須是進入同一個對象的 monitor 才有上述的效果

  • 不加 synchronized 的對象不會關聯監視器,不遵從以上規則

三、synchronized簡單原理

​ synchronized同步代碼塊原理:

public class SynchronizedCode {
    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }
}

以上代碼編譯過后通過Idea的jclasslib查看main()方法的字節碼如下:

 0 getstatic #2 <com/juc/synchronizedcode/SynchronizedCode.lock>
 3 dup
 4 astore_1
 5 monitorenter    # 進入同步方法,當前線程嘗試獲取對象鎖
 6 getstatic #3 <com/juc/synchronizedcode/SynchronizedCode.counter>
 9 iconst_1
10 iadd
11 putstatic #3 <com/juc/synchronizedcode/SynchronizedCode.counter>
14 aload_1
15 monitorexit   # 退出同步方法,當前線程釋放對象鎖
16 goto 24 (+8)
19 astore_2
20 aload_1
21 monitorexit  # 退出同步方法,當前線程釋放對象鎖
22 aload_2
23 athrow
24 return

有兩個退出同步方法的語句是因為為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行。多的那一個就是異常結束時被執行的釋放monitor 的指令。

方法級別的 synchronized 不會在字節碼指令中有所體現,它是隱式的,無需同果字節嗎指令看來控制

public class SynchronizedCode {
    public static void main(String[] args) {

    }
    public static  synchronized void test(){
            System.out.println("靜態代碼塊!");
    }
}

字節碼如下:

image-20201007195300461

四、synchronized鎖膨脹過程

synchronized的鎖膨脹是jdk1.6對synchronized的優化,鎖的狀態總共有四種,無鎖,偏向鎖,輕量級鎖,重量級鎖。鎖的升級是單向的,只能從低級的鎖升級到高級的鎖。鎖的膨脹過程為無鎖->偏向鎖->輕量級鎖->重量級鎖。

  1. 無鎖:當前沒有線程獲取到同步監視器就是無鎖狀態

  2. 偏向鎖:輕量級鎖在沒有競爭時(就自己一個線程在獲取鎖),每次重入仍然需要執行 CAS 操作,所以Java 6 中引入了偏向鎖來做進一步優化:只有第一次使用 CAS 將線程 ID 設置到對象的 Mark Word 頭,此時Mark Word 的結構也變為偏向鎖結構,之后當這個線程再次請求鎖時,發現這個線程 ID 是自己的就表示沒有競爭,不用重新 CAS。以后只要不發生競爭,這個對象就歸該線程所有。

    image-20201007202227730

    1. 如果開啟了偏向鎖(默認開啟),那么對象創建后,markword 值為 0x05 即最后 3 位為 101,這時它的thread、epoch、age 都為 0。
    2. 偏向鎖是默認是延遲的,不會在程序啟動時立即生效,如果想避免延遲,可以加 VM 參數 -XX:BiasedLockingStartupDelay=0 來禁用延遲。
    3. 如果沒有開啟偏向鎖,那么對象創建后,markword 值為 0x01 即最后 3 位為 001,這時它的 hashcode、age 都為 0,第一次用到 hashcode 時才會賦值

  3. 輕量級鎖:倘若偏向鎖失敗,虛擬機並不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級的鎖。如果一個對象雖然有多線程要加鎖,但加鎖的時間是錯開的(也就是同步周期沒有競爭),那么可以使用輕量級鎖來優化。輕量級鎖對使用者是透明的,即語法仍然是 synchronized。但如果存在同一時間多個線程訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。

    • 創建鎖記錄(Lock Record)對象,每個線程都的棧幀都會包含一個鎖記錄的結構,內部可以存儲鎖定對象的Mark Word

    image-20201007201720652

    • 讓鎖記錄中 Object reference 指向鎖對象,並嘗試用 cas 替換 Object 的 Mark Word,將 Mark Word 的值存入鎖記錄。

      image-20201007201742290

    • 如果 cas 替換成功,對象頭中存儲了 鎖記錄地址和狀態 00 ,表示由該線程給對象加鎖,這時圖示如下

      image-20201007201801998

    • 如果 cas 失敗,有兩種情況

      1. 如果是其它線程已經持有了該 Object 的輕量級鎖,這時表明有競爭,進入鎖膨脹過程

      2. 如果是自己執行了 synchronized 鎖重入,那么再添加一條 Lock Record 作為重入的計數

        image-20201007201850522

    • 當退出 synchronized 代碼塊(解鎖時)如果有取值為 null 的鎖記錄,表示有重入,這時重置鎖記錄,表示重入計數減一。

      image-20201007201914742

    • 當退出 synchronized 代碼塊(解鎖時)鎖記錄的值不為 null,這時使用 cas 將 Mark Word 的值恢復給對象頭,若成功,則解鎖成功,若失敗,說明輕量級鎖進行了鎖膨脹或已經升級為重量級鎖,進入重量級鎖的解鎖流程。

  4. 重量級鎖:如果在嘗試加輕量級鎖的過程中,CAS 操作無法成功,這時一種情況就是有其它線程為此對象加上了輕量級鎖(有競爭),這時需要進行鎖膨脹,將輕量級鎖變為重量級鎖。

    1. 當 Thread-1 進行輕量級加鎖時,Thread-0 已經對該對象加了輕量級鎖

      image-20201007203125120

    2. 這時 Thread-1 加輕量級鎖失敗,進入鎖膨脹流程,即為 Object 對象申請 Monitor 鎖,讓 Object 指向重量級鎖地址,然后自己進入 Monitor 的 EntryList BLOCKED。

      image-20201007203211871

    3. 當 Thread-0 退出同步塊解鎖時,使用 cas 將 Mark Word 的值恢復給對象頭,失敗。這時會進入重量級解鎖流程,即按照 Monitor 地址找到 Monitor 對象,設置 Owner 為 null,喚醒 EntryList 中 BLOCKED 線程。

  5. 自旋鎖優化:重量級鎖競爭的時候,還可以使用自旋來進行優化,如果當前線程自旋成功(即這時候持鎖線程已經退出了同步塊,釋放了鎖),這時當前線程就可以避免阻塞。因為線程狀態的切換比較耗時。

    自旋重試成功:

    image-20201007210323284

    自旋重試失敗:

    image-20201007210449713

    1. 自旋會占用 CPU 時間,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發揮優勢。
    2. 在 Java 6 之后自旋鎖是自適應的,比如對象剛剛的一次自旋操作成功過,那么認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋。
    3. Java 7 之后不能控制是否開啟自旋功能。

    6.鎖消除:消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間。

    //以下方法,同步代碼塊的鎖對象為局部變量,不可能存在競爭
    public void b() throws Exception {
     Object o = new Object();
     synchronized (o) {
     x++;
     }
    }
    

參考鏈接:https://blog.csdn.net/javazejian/article/details/72828483(絕對大佬,十分完善!)

參考鏈接:https://www.jianshu.com/p/3d38cba67f8b
參考資料:深入理解Java虛擬機第三版和黑馬


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM