
歡迎來到《并發(fā)王者課》,本文是該系列文章中的第5篇。
在前面的文章《青銅4:synchronized用法初體驗》中,我們已經(jīng)提到鎖的概念,并指出synchronized是鎖機制的一種實現(xiàn)。可是,這么說未免太過抽象,你可能無法直觀地理解鎖究竟是什么?所以,本文會粗略地介紹synchronized背后的一些基本原理,讓你對Java中的鎖有個粗略但直觀的印象。
本文將分兩個部分,首先你要從Mark Word中認識鎖,因為對象鎖的信息存在于Mark Word中,其次通過JOL工具實際體驗Mark Word的變化。
一、從Mark Word認識鎖
我們知道,在HotSpot虛擬機中,一個對象的存儲分布由3個部分組成:
- 對象頭(Header):由Mark Word和Klass Pointer組成;
- 實例數(shù)據(jù)(Instance Data):對象的成員變量及數(shù)據(jù);
- 對齊填充(Padding):對齊填充的字節(jié),暫時不必理會。
在這3個部分中,對象頭中的Mark Word是本文的重點,也是理解Java鎖的關(guān)鍵。Mark Word記錄的是對象運行時的數(shù)據(jù),其中包括:
- 哈希碼(identity_hashcode)
- GC分代年齡(age)
- 鎖狀態(tài)標(biāo)志
- 線程持有的鎖
- 偏向線程ID(thread)
所以,從對象頭中的Mark Word看,Java中的鎖就是對象頭中的一種數(shù)據(jù)。在JVM中,每個對象都有這樣的鎖,并且用于多線程訪問對象時的并發(fā)控制。
如果一個線程想訪問某個對象的實例,那么這個線程必須擁有該對象的鎖。首先,它需要通過對象頭中的Mark Word判斷該對象的實例是否已經(jīng)被線程鎖定。如果沒有鎖定,那么線程會在Mark Word中寫入一些標(biāo)記數(shù)據(jù),就是告訴別人:這個對象是我的啦!如果其他線程想訪問這個實例的話,就需要進入等待隊列,直到當(dāng)前的線程釋放對象的鎖,也就是把Mark Word中的數(shù)據(jù)擦除。
當(dāng)一個線程擁有了鎖之后,它便可以多次進入。當(dāng)然,在這個線程釋放鎖的時候,那么也需要執(zhí)行相同次數(shù)的釋放動作。比如,一個線程先后3次獲得了鎖,那么它也需要釋放3次,其他線程才可以繼續(xù)訪問。
下面的表格展示的是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內(nèi)存布局查看工具,希望你能記住它。
首先,在工程中引入依賴:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
在下面的代碼中,master是我們創(chuàng)建的對象實例,方法decreaseBlood()中會執(zhí)行加鎖動作。所以,在調(diào)用decreaseBlood()加鎖后,對象頭信息應(yīng)該會發(fā)生變化。
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());
}
}
結(jié)果輸出如下:
====加鎖前====
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
從結(jié)果中可以看到,代碼在執(zhí)行synchronized方法后,所打印出的object header信息由01 00 00 00、00 00 00 00變成了48 f9 d6 00、00 70 00 00等等,不出意外的話,相信你應(yīng)該看不明白這些內(nèi)容的含義。
所以,為了方便閱讀,我們在青銅系列文章《借花獻佛-JOL格式化工具》中提供了一個工具類,讓輸出更具可讀性。借助工具類,我們把代碼調(diào)整為:
public static void main(String[] args) {
Master master = new Master();
System.out.println("====加鎖前====");
printObjectHeader(master);
System.out.println("====加鎖后====");
synchronized (master) {
printObjectHeader(master);
}
}
輸出的結(jié)果如下:
====加鎖前====
# 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
你看,這樣一來,輸出的結(jié)果的結(jié)果就一目了然。從加鎖后的結(jié)果中可以看到,Mark Word已經(jīng)發(fā)生變化,當(dāng)前線程已經(jīng)獲得對象的鎖。
至此,你應(yīng)該明白,原來synchronized的背后的原理是這么回事。當(dāng)然,本文所講述只是其中的部分。出于篇幅考慮和難度控制,本文暫且不會對Java對象頭中鎖的含義和鎖的升級等問題展開描述,這部分內(nèi)容會在后面的文章中詳細介紹。
以上就是文本的全部內(nèi)容,恭喜你又上了一顆星?
夫子的試煉
- 下載JOL工具,在代碼中體驗工具的使用和對象信息的變化。
關(guān)于作者
關(guān)注【技術(shù)八點半】,及時獲取文章更新。傳遞有品質(zhì)的技術(shù)文章,記錄平凡人的成長故事,偶爾也聊聊生活和理想。早晨8:30推送作者品質(zhì)原創(chuàng),晚上20:30推送行業(yè)深度好文。
如果本文對你有幫助,歡迎點贊、關(guān)注、監(jiān)督,我們一起從青銅到王者。