在前面的文章《青銅4:synchronized用法初體驗》中,我們已經提到鎖的概念,並指出synchronized
是鎖機制的一種實現。可是,這麼說未免太過抽象,你可能無法直觀地理解鎖究竟是什麼?所以,本文會粗略地介紹synchronized
背後的一些基本原理,讓你對Java中的鎖有個粗略但直觀的印象。
本文將分兩個部分,首先你要從Mark Word中認識鎖,因為物件鎖的資訊存在於Mark Word中,其次通過JOL工具實際體驗Mark Word的變化。
一、從Mark Word認識鎖
我們知道,在HotSpot虛擬機器中,一個物件的儲存分佈由3個部分組成:
- 物件頭(Header):由Mark Word和Klass 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 00
、00 00 00 00
變成了48 f9 d6 00
、00 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工具,在程式碼中體驗工具的使用和物件資訊的變化。
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不兜售課程。
如果本文對你有幫助,歡迎點贊、關注。