併發王者課-青銅5:一探究竟-如何從synchronized理解Java物件頭中的鎖

秦二爺發表於2021-05-27

在前面的文章《青銅4:synchronized用法初體驗》中,我們已經提到的概念,並指出synchronized是鎖機制的一種實現。可是,這麼說未免太過抽象,你可能無法直觀地理解鎖究竟是什麼?所以,本文會粗略地介紹synchronized背後的一些基本原理,讓你對Java中的鎖有個粗略但直觀的印象。

本文將分兩個部分,首先你要從Mark Word中認識鎖,因為物件鎖的資訊存在於Mark Word中,其次通過JOL工具實際體驗Mark Word的變化。

一、從Mark Word認識鎖

我們知道,在HotSpot虛擬機器中,一個物件的儲存分佈由3個部分組成:

  • 物件頭(Header):由Mark WordKlass Pointer組成;
  • 例項資料(Instance Data):物件的成員變數及資料;
  • 對齊填充(Padding):對齊填充的位元組,暫時不必理會。

在這3個部分中,物件頭中的Mark Word是本文的重點,也是理解Java鎖的關鍵。Mark Word記錄的是物件執行時的資料,其中包括:

  • 雜湊碼(identity_hashcode)
  • GC分代年齡(age)
  • 鎖狀態標誌
  • 執行緒持有的鎖
  • 偏向執行緒ID(thread)

所以,從物件頭中的Mark Word看,Java中的鎖就是物件頭中的一種資料。在JVM中,每個物件都有這樣的鎖,並且用於多執行緒訪問物件時的併發控制。

如果一個執行緒想訪問某個物件的例項,那麼這個執行緒必須擁有該物件的。首先,它需要通過物件頭中的Mark Word判斷該物件的例項是否已經被執行緒鎖定。如果沒有鎖定,那麼執行緒會在Mark Word中寫入一些標記資料,就是告訴別人:這個物件是我的啦!如果其他執行緒想訪問這個例項的話,就需要進入等待佇列,直到當前的執行緒釋放物件的鎖,也就是把Mark Word中的資料擦除。

當一個執行緒擁有了鎖之後,它便可以多次進入。當然,在這個執行緒釋放鎖的時候,那麼也需要執行相同次數的釋放動作。比如,一個執行緒先後3次獲得了鎖,那麼它也需要釋放3次,其他執行緒才可以繼續訪問。

下面的表格展示的是64位計算機中的物件頭資訊:

|------------------------------------------------------------------------------------------------------------|--------------------|
|                                            Object Header (128 bits)                                        |        State       |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                                  Mark Word (64 bits)                         |    Klass Word (64 bits)     |                    |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |    OOP to metadata object   |       Normal       |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |    OOP to metadata object   |       Biased       |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 |    OOP to metadata object   | Lightweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 |    OOP to metadata object   | Heavyweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
|                                                                     | lock:2 |    OOP to metadata object   |    Marked for GC   |
|------------------------------------------------------------------------------|-----------------------------|--------------------|

從表格中,你可以看到Object Header中的三部分資訊:Mark Word、Klass Word、State.

二、通過JOL體驗Mark Word的變化

為了直觀感受物件頭中Mark Word的變化,我們可以通過 JOL(Java Object Layout) 工具演示一遍。JOL是一個不錯的Java記憶體佈局檢視工具,希望你能記住它。

首先,在工程中引入依賴:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

在下面的程式碼中,master是我們建立的物件例項,方法decreaseBlood()中會執行加鎖動作。所以,在呼叫decreaseBlood()加鎖後,物件頭資訊應該會發生變化

 public static void main(String[] args) {
        Master master = new Master();
        System.out.println("====加鎖前====");
        System.out.println(ClassLayout.parseInstance(master).toPrintable());
        System.out.println("====加鎖後====");
        synchronized (master) {
            System.out.println(ClassLayout.parseInstance(master).toPrintable());
        }
    }

結果輸出如下:

====加鎖前====
cn.tao.king.juc.execises1.Master object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int Master.blood                              100
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

====加鎖後====
cn.tao.king.juc.execises1.Master object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           48 f9 d6 00 (01001000 11111001 11010110 00000000) (14088520)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int Master.blood                              95
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


Process finished with exit code 0

從結果中可以看到,程式碼在執行synchronized方法後,所列印出的object header資訊由01 00 00 0000 00 00 00變成了48 f9 d6 0000 70 00 00等等,不出意外的話,相信你應該看不明白這些內容的含義。

所以,為了方便閱讀,我們在青銅系列文章《借花獻佛-JOL格式化工具》中提供了一個工具類,讓輸出更具可讀性。藉助工具類,我們把程式碼調整為:

 public static void main(String[] args) {
        Master master = new Master();
        System.out.println("====加鎖前====");
        printObjectHeader(master);
        System.out.println("====加鎖後====");
        synchronized (master) {
            printObjectHeader(master);
        }
    }

輸出的結果如下:

====加鎖前====
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
Class Pointer: 11111000 00000000 11000001 01000011 
Mark Word:
	hashcode (31bit): 0000000 00000000 00000000 00000000 
	age (4bit): 0000
	biasedLockFlag (1bit): 0
	LockFlag (2bit): 01

====加鎖後====
Class Pointer: 11111000 00000000 11000001 01000011 
Mark Word:
	javaThread*(62bit,include zero padding): 00000000 00000000 01110000 00000000 00000100 11100100 11101001 100100
	LockFlag (2bit): 00

你看,這樣一來,輸出的結果的結果就一目瞭然。從加鎖後的結果中可以看到,Mark Word已經發生變化,當前執行緒已經獲得物件的鎖。

至此,你應該明白,原來synchronized的背後的原理是這麼回事。當然,本文所講述只是其中的部分。出於篇幅考慮和難度控制,本文暫且不會對Java物件頭中鎖的含義和鎖的升級等問題展開描述,這部分內容會在後面的文章中詳細介紹。

以上就是文字的全部內容,恭喜你又上了一顆星✨

夫子的試煉

  • 下載JOL工具,在程式碼中體驗工具的使用和物件資訊的變化。

關於作者

關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不兜售課程。

如果本文對你有幫助,歡迎點贊關注

相關文章