[toc]
在前面聊過了如何使用synchronized,以及synchronized不同的加鎖方式分別鎖的是哪些對象。本文對synchronized底層的原理進(jìn)行深層次的分析。
1.java對象的內(nèi)存布局
再前面學(xué)習(xí)了JMM之后,做為一個java程序員,肯定最大的疑問在于,一個java對象,究竟再內(nèi)存中是如何存儲的?因此,我們需要用到一個三方的jar包工具jol來對java對象進(jìn)行查看。
1.1 導(dǎo)入jol
導(dǎo)入的方式比較簡單,我們只需要在pom文件中添加如下內(nèi)容即可:
<!-- 查看內(nèi)存布局-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
之后就可以使用jol來查看對象的內(nèi)存布局了。
1.2 空對象的內(nèi)存布局
首先我們來查看一個Object空對象的內(nèi)存布局:
public class SynchronizedTest {
public static void main(String[] args) {
Object o = new Object();
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
}
執(zhí)行上述代碼,將輸出如下內(nèi)容:
java.lang.Object 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) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到,輸出結(jié)果一共有4行,輸出結(jié)果分別是OFFSET表示開始的偏移量,SIZE表示大小。我們可以看到,前三行都是object header。表示對象的頭文件。而前面的兩行是對象頭markword。第三行的4個字節(jié)是對象指針。由于該對象是一個空對象,那么最后的4個字節(jié)實際上是空的,在此只是為了對齊所用。

需要注意的是,在java中,對象指針默認(rèn)是可以壓縮的。我們可以用-XX:-UseCompressedClassPointers來關(guān)閉,那么此時對象指針就有8個字節(jié)。

java.lang.Object 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) 00 1c fd 1d (00000000 00011100 11111101 00011101) (503127040)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

。
1.3 數(shù)組的對象布局
在java中,數(shù)組實際上是一個特殊的對象,我們來看看數(shù)組的對象布局:
public class SynchronizedTest {
public static void main(String[] args) {
Object [] o = new Object[10];
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
}
其輸出:
[Ljava.lang.Object; 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) 4c 23 00 f8 (01001100 00100011 00000000 11111000) (-134208692)
12 4 (object header) 0a 00 00 00 (00001010 00000000 00000000 00000000) (10)
16 40 java.lang.Object Object;.<elements> N/A
Instance size: 56 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以發(fā)現(xiàn),數(shù)組對象其header中會多一行,第四行,其中存的是數(shù)組的長度。在此時輸出為10。
1.4 synchronized之后的對象布局
我們現(xiàn)在來測試將object加鎖,再看看結(jié)果:
public class SynchronizedTest {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
}
}
輸出結(jié)果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) d0 f5 e4 04 (11010000 11110101 11100100 00000100) (82114000)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到,MarkWord明顯不同于前面的情況。第一行中的值發(fā)生了明顯的變化。因此,synchronized實際上是通過修改MarkWord的值來實現(xiàn)其加索的。
實際上這一點也非常好理解,如果需要對Object對象加鎖,那么最簡單的辦法就是在這個對象的MarkWord上做一個標(biāo)記。至于加鎖的細(xì)節(jié),我們來詳細(xì)對MarkWord進(jìn)行分析。
2.MarkWord
通過前面部分的內(nèi)容,不難發(fā)現(xiàn),再java對象中,有個關(guān)鍵的內(nèi)容就是對象頭中的MarkWord部分。
實際上,對于markWord的控制,一共有5種情況。
需要注意的是,MarkWord小端在前。
MarkWord分別對應(yīng)五種狀態(tài)。64bit的MarkWord如下表:

但是有的版本32位的jdk也是采用的32bit的MarkWord。

上述五種狀態(tài)分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖、GC回收之后的標(biāo)記。
上圖中的epoch,是偏向鎖的時間戳。
我們再來對比之前執(zhí)行的結(jié)果。
空對象的結(jié)果:
java.lang.Object 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) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到第一個字節(jié)的最后一位是1。為什么不是第二個字節(jié)的最后一位呢,按上表的描述,最后兩個字節(jié)為01表示無鎖。但是需要注意的是,jvm采用的是小端模式,數(shù)據(jù)的高字節(jié)存儲再高地址中,低字節(jié)存儲再低地址中。但是需要注意的是,這里每次輸出的都是4個字節(jié),再第一行的內(nèi)部,jol已經(jīng)幫我們做了處理。因此現(xiàn)在看起來第一行的最后兩位才是我們上表中的鎖狀態(tài)位。
3.synchronized的鎖升級簡介
再synchronized的執(zhí)行過程中,實際上一個對象的狀態(tài)就如上表所示進(jìn)行變化:
- 無鎖:所有對象創(chuàng)建的時候都是無鎖狀態(tài)。此時MarkWord上只有一個標(biāo)識,沒有其他內(nèi)容。
- 偏向鎖:如果我們需要對一個無鎖的對象加鎖,那么最初始的操作非常簡單,通過cas操作在其MarkWord上修改偏向鎖狀態(tài)為1,之后將線程的ID和epoch存儲在MarkWord中。偏向鎖是采用cas操作的,只有遇到其他線程競爭的時候,才會釋放。
- 輕量級鎖:當(dāng)鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。當(dāng)加了偏向鎖的對象,有其他線程也參與其鎖的競爭的時候,此時,就會將偏向鎖撤銷,然后再判斷是否需要變成輕量級鎖。此時也是通過cas操作,將鎖標(biāo)識位修改為00。并將指向棧中記錄的指針寫入markWord中。
- 重量級鎖:當(dāng)多個線程競爭同一個鎖的時候,虛擬機會阻塞加鎖失敗的線程,并將在目標(biāo)被鎖釋放的時候,喚醒這個線程。java線程的阻塞與喚醒,都是依賴于系統(tǒng)操作os pthread_mutex_lock() 。當(dāng)升級為重量級的鎖之后,鎖的標(biāo)識狀態(tài)為10,此時MarkWord中存儲的是指向重量級鎖的指針。其他的等待線程都會進(jìn)入阻塞狀態(tài)。
- GC狀態(tài):標(biāo)記之后等待GC回收的對象。
這就是synchronized鎖升級的過程:

需要注意的是:
- 偏向鎖只會在第一次請求的時候采用cas操作,修改鎖的對象和記錄線程的地址。在之后的運行過程中,持有該偏向所的線程再次加鎖就會直接返回。偏向鎖僅僅只針對同一線程持有鎖的情況。
- 輕量級鎖采用cas操作,將鎖的對象標(biāo)記字段替換為一個指針,指向當(dāng)前線程棧上的一塊空間。存儲著鎖對象原本的標(biāo)記字段。他針對的是多個線程在不同時間段同時請求同一個鎖的情況。
- 重量級鎖實際上通過系統(tǒng)調(diào)用0x80操作,會阻塞其他線程,針對的是多個線程同時競爭同一個鎖的情況,java虛擬機采用了自適應(yīng)的自旋操作,避免線程進(jìn)行不必要的阻塞和喚醒的情況。
3.synchronized的字節(jié)碼
我們通過javap來看看前文中的SynchronizedTest.class的內(nèi)容
$ javap -c -l SynchronizedTest
????: ?????????SynchronizedTest????com.dhb.test.SynchronizedTest
Compiled from "SynchronizedTest.java"
public class com.dhb.test.SynchronizedTest {
public com.dhb.test.SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/dhb/test/SynchronizedTest;
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: aload_1
13: invokestatic #3 // Method org/openjdk/jol/info/ClassLayout.parseInstance:(Ljava/lang/Object;)Lorg/openjdk/jol/info/ClassLayout;
16: invokevirtual #4 // Method org/openjdk/jol/info/ClassLayout.toPrintable:()Ljava/lang/String;
19: astore_3
20: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_3
24: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: aload_2
28: monitorexit
29: goto 39
32: astore 4
34: aload_2
35: monitorexit
36: aload 4
38: athrow
39: return
Exception table:
from to target type
12 29 32 any
32 36 32 any
LineNumberTable:
line 9: 0
line 10: 8
line 11: 12
line 12: 20
line 13: 27
line 14: 39
LocalVariableTable:
Start Length Slot Name Signature
20 7 3 s Ljava/lang/String;
0 40 0 args [Ljava/lang/String;
8 32 1 o Ljava/lang/Object;
}
可以發(fā)現(xiàn),在輸出結(jié)果中,synchronized的本質(zhì),實際上是轉(zhuǎn)換為了monitorenter和兩個monitorexit字節(jié)碼。之所以有兩個字節(jié)碼是因為需要對正常和異常兩條路徑都確保能夠monitorexit退出。
monitorenter和monitorexit指令都是在hotSpot源碼的objectMonitor.cpp中。后續(xù)將通過源碼,對synchronized的加鎖和升級過程進(jìn)行分析。