程序員你真的足夠了解synchronized嗎?對象的內存結構和鎖升級關係瞭解嗎?

開篇思考

  1. 對象在堆中的數據結構?和鎖有什麼關係?
  2. 對象的鎖是如何升級的?

還是繞不開底層。曾經一遍遍來自靈魂的追問,別再深入了,又不是爲愛"鼓掌",有樂趣嗎? 嘿,還真的越深入越有趣。 其實對象鎖是由 Synchronized 來進行操控的,因爲由虛擬機運行加鎖步驟,而且各種解釋都是非常抽象,
所以很多人對底層加鎖原理不是很理解。其實這個可以參考 JUC 裏面提供的手動加鎖機制來作爲參考。 如果想理解手動加鎖過程,可以看看這篇介紹《AQS 都不懂怎麼能說懂併發?AQS 實現手動加鎖原理分析》

對象在堆的內存結構

JVM 中的堆內存我們都知道是用來存儲 Java 實例化對象的。到底存儲了什麼呢?用來存放動態產生的數據,比如 new 出來的對象。 注意:創建出來的對象只包含屬於各自的成員變量,並不包括成員方法。 因爲同一個類的對象擁有各自的成員變量,存儲在各自的堆中,但是他們共享該類的方法,並不是創建新的對象就複製一份方法,方法都保存在方法區中。 在堆中只會存儲成員方法的地址,在調用的時候,根據地址去方法區中執行對應的成員方法。

堆中的數據結構:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nP3T7EcL-1587362736305)(https://torgor.github.io/styles/images/jmm/jvm-heap-oop-structure.png)]

由上圖可以看出 Java 對象保存在內存中時,由以下三部分組成:

  1. 對象頭 : MarkWord,對象指針,鎖標誌位,GC分代年齡。
  2. 實例數據 :用來保存 new 出來的具體實例數據,比如屬於實例對象的屬性值:用戶名=張三、性別=男、身份證號=XXX
  3. 對齊填充字節:這個不是必要,因爲內存的使用都會被填充爲八字節的倍數,純粹是爲了補位。
    數組對象是有些特別的,會在頭中多一個 int 類型 lenth 來表示數組長度。

對象頭內存結構和鎖

常常說 synchronized 鎖住對象,那麼具體怎麼鎖的,通過什麼來判斷鎖類型? 其實在 jdk1.6 之前的 synchronized 鎖都是重量級鎖,從 jdk1.6 開始對鎖進行了優化,
加入了從無鎖-偏向鎖-輕量級鎖-自旋-重量級鎖的升級流程。
如果需要了解更多關於鎖的概念,看這篇關於鎖的文章《聊聊你知道的鎖》

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QLoKXFYH-1587362736307)(https://torgor.github.io/styles/images/jmm/heap-object-head-structure.png)]

64位的數據結構略有不同: 無鎖狀態 25bits unused ,31bits hashcode; 偏向鎖狀態 線程ID 54bits,其餘和 32 位相同
由上圖我們看到了頭部 MarkWord 中的內存結構,當無鎖和偏向鎖的時候,鎖標誌位都是 01 ,
只有偏向鎖的時候前面內存中保存了線程的 ID ,用來判斷下次進入鎖的時候,是否是當前線程。
如是直接獲取鎖,性能很好,這也是偏向鎖的原理。
偏向鎖會偏向於前一個獲得它的線程,在接下來的線程競爭執行過程中,假如該鎖沒有被其他線程所獲取,
沒有其他線程來競爭該鎖,那麼持有偏向鎖的線程不需要進行同步加鎖操作,可以認爲 01 狀態的都是一種無鎖狀態。

偏向鎖是可以通過參數設置是否開啓的,當線程不是偏向鎖,而且又有鎖競爭,就會 CAS 比較替換對象頭中的 threadId,
如果替換成功,還是偏向鎖。
如果不成功,說明有鎖競爭,進行自旋等待,升級爲輕量級鎖。 如果還是競爭失敗,升級爲重量級鎖。

加鎖過程圖(太長了畫不下,精簡了)

偏向鎖的撤銷

如果升級輕量級鎖,那麼偏向鎖就應該失效才行,鎖失效撤銷的過程大致如下:

  1. 在一個安全點停止擁有鎖的線程。
  2. 遍歷線程棧,如果存在鎖記錄的話,需要修復鎖記錄和Markword,使其變成無鎖狀態。
  3. 喚醒當前線程,將當前鎖升級成輕量級鎖。
    所以爲了避免性能問題,我們需要分析代碼中是否要經常兩個不同線程同時爭搶同步代碼執行,如果是需要關閉偏向鎖提高性能。
    偏向鎖 JVM 參數:
啓用參數: -XX:+UseBiasedLocking  
關閉延遲: -XX:BiasedLockingStartupDelay=0 禁用參數: -XX:-UseBiasedLocking  

輕量級鎖升級 偏向鎖撤銷升級爲輕量級鎖,對象的Markword也會進行相應的的變化。

  1. 線程在自己的棧楨中創建鎖記錄 LockRecord。
  2. 將鎖對象的對象頭中的 MarkWord 複製到線程的剛剛創建的鎖記錄中。
  3. 將鎖記錄中的 Owner 指針指向鎖對象。
  4. 將鎖對象的對象頭的MarkWord替換爲指向鎖記錄的指針。

重量級鎖的具體實現

重量級鎖依賴 JVM 中的 monitor 對象來實現。 接下來我們以對象鎖來分析,我們都知道 synchronized(this) 就是給對象上鎖,this 就是指具體的對象。 通過反編譯可以看到有兩個個關鍵字: 1. monitorenter
2. monitorexit

monitorenter 每一個對象都會和一個監視器monitor關聯。監視器被佔用時會被鎖住,其他線程無法來獲取該monitor。
當 JVM 執行某個線程的某個方法內部的 monitorenter 時,它會嘗試去獲取當前對象對應的 monitor 的所有權。其過程如下:

  1. 若monior的進入數爲0,線程可以進入 monitor,並將monitor的進入數置爲1。當前線程成爲 monitor 的持有者 2. 若線程已擁有monitor的所有權,允許它重入monitor,並遞增monitor的進入數
  2. 若其他線程已經佔有monitor的所有權,那麼當前嘗試獲取monitor的所有權的線程會被阻塞,直到monitor的進入數變爲0,
    才能重新嘗試獲取monitor的所有權。

monitorexit 1. 能執行 monitorexit 指令的線程一定是擁有當前對象的 monitor 的所有權的線程。
2. 執行 monitorexit 時會將 monitor 的進入數 -1。當monitor的進入數減爲0時,當前線程退出monitor,
不再擁有 monitor 的所有權,此時其他被這個 monitor 阻塞的線程可以嘗試去獲取這個 monitor 的所有權。

如下是針對 synchronized 關鍵字的示例代碼:

public class MySynchronizedTest {
    public MySynchronizedTest() {
    }

    public synchronized void testMonitor() {
        System.out.println("synchronized method");
    }

    public void testSynchronizedThis() {
        synchronized(this) {
            System.out.println("synchronized this");
        }
    }

    public static synchronized void testStatic() {
        System.out.println("synchronized static");
    }
}

javap -v MySynchronizedTest.class 命令查看編譯後的內容:

public class com.holy.nacosconsumer.MySynchronizedTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   ...
{
  public com.holy.nacosconsumer.MySynchronizedTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/holy/nacosconsumer/MySynchronizedTest;

  public synchronized void testMonitor();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String synchronized method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/holy/nacosconsumer/MySynchronizedTest;

  public void testSynchronizedThis();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #5                  // String synchronized this
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 10: 0
        line 11: 4
        line 12: 12
        line 13: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  this   Lcom/holy/nacosconsumer/MySynchronizedTest;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class com/holy/nacosconsumer/MySynchronizedTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public static synchronized void testStatic();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String synchronized static
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 16: 0
        line 17: 8
}

synchronized 相關知識樹

在這裏插入圖片描述

喜歡文章請關注我

程序領域 點擊關注+轉發,私信發送【面試】或者【資料】可以收穫更多資源

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章