實(shí)現(xiàn)synchronized的基礎(chǔ)有兩個(gè):Java 對(duì)象頭和 Monitor。
在虛擬機(jī)規(guī)范中定義了對(duì)象在內(nèi)存中的布局,主要由以下 3 部分組成:
- 對(duì)象頭
- 實(shí)例數(shù)據(jù)
- 對(duì)齊填充
而synchronized的實(shí)現(xiàn)就藏在對(duì)象頭中。對(duì)象頭中由兩個(gè)比較重要的部分組成:
Mark Word:默認(rèn)存儲(chǔ)對(duì)象的 hashCode,分代年齡,鎖類型,鎖標(biāo)志位等信息,是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵
Class Metadata Address:類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM 通過(guò)這個(gè)指針確定該對(duì)象是哪個(gè)類的數(shù)據(jù)
下圖是在 32 位機(jī)器上的 Mark Word 的組成示意圖。在 Java6 之前,synchronized的實(shí)現(xiàn)是依靠重量級(jí)鎖來(lái)實(shí)現(xiàn)的,鎖標(biāo)志位是10。在 Java6后,對(duì)synchronized進(jìn)行了優(yōu)化,增加了輕量級(jí)鎖和偏向鎖
<div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/6/1714e3c8c13a7ae7?w=980&h=288&f=png&s=32490"/> </div>
我們先來(lái)說(shuō)一下重量級(jí)鎖:重量級(jí)鎖中存放的是指向重量級(jí)鎖的指針。在 Java 中,每個(gè)對(duì)象都存在著一個(gè) Monitor 與之關(guān)聯(lián)。當(dāng)線程持有一個(gè)對(duì)象的 Monitor 后,Monitor便處于鎖定狀態(tài)。在 Hotspot 中,Monitor 是由ObjectMonitor來(lái)實(shí)現(xiàn)的。
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
上面是ObjectMonitor類的初始化代碼,可以看到有_WaitSet和_EntryList,對(duì)應(yīng)于 Java 中的等待池和鎖池
-
_owner表示的是持有objectMonitor的線程。當(dāng)多個(gè)線程同時(shí)訪問(wèn)一個(gè)對(duì)象的同步代碼時(shí),首先會(huì)進(jìn)入到_EntryList里,當(dāng)線程獲取到對(duì)象的Monitor后,就把Monitor中的owner設(shè)置位當(dāng)前線程,同時(shí)Monitor中的_count變量+1 - 若持有
Monitor的線程執(zhí)行完畢或者調(diào)用wait()方法,則會(huì)釋放持有的Monitor,owner會(huì)被設(shè)置為NULL,并將 count-1。當(dāng)前線程會(huì)進(jìn)入到_WaitSet,等待被喚醒
Monitor對(duì)象存在于每個(gè)對(duì)象的對(duì)象頭中,synchronized便是通過(guò)持有Monitor實(shí)現(xiàn)鎖的,這也是為什么 Java 中任意對(duì)象可以作為鎖的原因。
下面是從字節(jié)碼層面來(lái)分析synchronized關(guān)鍵字
public class SyncBlockAndMethod {
public void syncTask() {
//同步代碼塊
synchronized (this) {
System.out.println("Hello");
}
}
//同步方法
public void syncMethod() {
System.out.println("Hello Again");
}
}
在類中定義了同步代碼塊和同步方法,使用javac編譯為字節(jié)碼
javac SyncBlockAndMethod.java
然后使用javap查看編譯生成的字節(jié)碼
javap -verbose SyncBlockAndMethod
首先來(lái)看與synchronized修飾同步代碼塊有關(guān)的字節(jié)碼
public void syncTask();
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 #3 // String Hello
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
...
...
...
從字節(jié)碼中可知,synchronized同步語(yǔ)句塊的實(shí)現(xiàn)主要對(duì)應(yīng)于monitorenter和monitorexit指令。monitorenter是同步代碼塊的入口,表示在這里獲取鎖。monitorexit是同步代碼塊的出口,表示在這里釋放鎖。
細(xì)心的同學(xué)可以看到字節(jié)碼中有一個(gè)monitorenter指令,卻有兩個(gè)monitorexit指令。第一個(gè)monitorexit指令是和monitorenter指令對(duì)應(yīng)的。
而為了保證在同步代碼塊中拋出異常時(shí)依然能夠釋放鎖,編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理器,在同步代碼塊中拋出異常時(shí),會(huì)在這個(gè)異常處理器里面釋放鎖,對(duì)應(yīng)于字節(jié)碼中的第二個(gè)monitorexit指令。
下面來(lái)看與synchronized修飾方法有關(guān)的字節(jié)碼
public synchronized void syncMethod();
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 #5 // String Hello Again
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 13: 0
line 15: 8
上面沒(méi)有看到monitorenter和monitorexit指令,因?yàn)樵?code>synchronized修飾方法時(shí),monitor的實(shí)現(xiàn)是隱式的,在方法的flags中添加了ACC_SYNCHRONIZED標(biāo)志位,通過(guò)這種方式來(lái)實(shí)現(xiàn)synchronized方法。當(dāng)一個(gè)線程進(jìn)入synchronized方法時(shí)會(huì)獲取monitor對(duì)象,在方法體中拋出異?;蛘邎?zhí)行完成時(shí)會(huì)釋放monitor。
什么是重入:
從互斥鎖的設(shè)計(jì)上來(lái)說(shuō),當(dāng)一個(gè)線程視圖操作一個(gè)由其他線程持有的對(duì)象鎖的臨界資源時(shí),將會(huì)處于阻塞狀態(tài)。但是當(dāng)一個(gè)線程再次請(qǐng)求自己持有對(duì)象鎖的臨界資源時(shí),這種情況屬于重入,是可以請(qǐng)求成功的。
舉個(gè)例子:一個(gè)線程在獲得鎖進(jìn)入synchronized方法時(shí),在方法內(nèi)部又調(diào)用了該對(duì)象另一個(gè)synchronized方法,是可以調(diào)用成功的。
在早期版本中,synchronized屬于重量鎖,Monitor依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn)
線程獲得鎖之后會(huì)切換,而線程之間的切換需要用用戶態(tài)轉(zhuǎn)換到和心態(tài),開(kāi)銷較大
在 Java6 以后,在 JVM(Hospot) 層面對(duì)synchronized做了較大的優(yōu)化,synchronized性能得到了很大的提升,包括如下:
- Adaptive Spinning(自適應(yīng)自旋鎖)
- Lock Eliminate(鎖消除)
- Lock Coarsening(鎖粗化)
- Lightweight Locking(輕量級(jí)鎖)
- Biased Locking(偏向鎖)
這些優(yōu)化都是為了在線程之間更高效地共享數(shù)據(jù)以及解決競(jìng)爭(zhēng)問(wèn)題,從而提高程序的運(yùn)行效率
自旋鎖
在許多情況下,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短一段時(shí)間,為了這一點(diǎn)點(diǎn)的時(shí)間去切換線程并不值得,在多核 CPU 的情況下,完全可以讓另一個(gè)沒(méi)有獲取到鎖的線程通過(guò)執(zhí)行忙循環(huán)等待鎖的釋放,而不是切換線程讓出 CPU。這就是自旋鎖,在 Java4 就引入了,只是默認(rèn)是關(guān)閉的,到了 Java6 之后才變?yōu)槟J(rèn)開(kāi)啟自旋鎖。
如果其他線程占有鎖的時(shí)間非常短,那么自旋很快就能獲取到鎖,自旋鎖的性能會(huì)很好;但是如果鎖被其他線程長(zhǎng)期占有,那么性能上的開(kāi)銷就會(huì)比較大,因?yàn)樽孕褪茄h(huán),如果循環(huán)時(shí)間較長(zhǎng),那么就會(huì)白白消耗 CPU 的資源。因此在 自旋一定次數(shù)后仍然沒(méi)有獲取到鎖,那么就使用傳統(tǒng)的方式掛起并切換線程,可以使用preBlockSpin參數(shù)設(shè)置自旋次數(shù)。但是要確定不同場(chǎng)景下自旋次數(shù)是比較困難的,因此出現(xiàn)了自適應(yīng)自旋鎖
自適應(yīng)自旋鎖
在自適應(yīng)自旋鎖中,自旋的次數(shù)不再固定,而是由上一次在同一個(gè)鎖上的自旋時(shí)間以及鎖的擁有者的狀態(tài)來(lái)決定的。如果在一個(gè)鎖對(duì)象上,剛剛有一個(gè)線程通過(guò)自旋成功獲取到鎖,并且持有鎖的線程正在運(yùn)行黃總,JVM 會(huì)認(rèn)為通過(guò)自旋成功獲取到鎖的可能性很大,在下次其他線程獲取鎖時(shí)會(huì)增加自旋的次數(shù)。反之如果一個(gè)鎖很少可以通過(guò)自旋成功獲取,那么在之后獲取鎖時(shí),將跳過(guò)自旋過(guò)程,避免浪費(fèi)處理器資源。
鎖消除
鎖消除也是一種鎖優(yōu)化,在進(jìn)行 JIT 編譯時(shí),JVM 對(duì)運(yùn)行上下文進(jìn)行掃描,去除不可能存在競(jìng)爭(zhēng)的鎖,可以節(jié)省一些沒(méi)有意義的請(qǐng)求鎖的時(shí)間,提升程序的性能。在下面的例子中,StringBuffer 是線程安全的,append() 方法帶有synchronized關(guān)鍵字修飾,但是由于 sb 對(duì)象只會(huì)在add()方法內(nèi)被調(diào)用,屬于局部變量,不可能b被其他線程引用,因此 sb 對(duì)象屬于不可能共享的資源,JVM 會(huì)自動(dòng)消除 sb.append() 方法的鎖
public class StringBufferWithoutSync {
public void add(String str1, String str2) {
StringBuffer sb = new StringBuffer();
//StringBuffer 是線程安全的,append() 方法帶有synchronized關(guān)鍵字修飾。
//但是由于 sb 對(duì)象只會(huì)在add()方法內(nèi)被調(diào)用,屬于局部變量,不可能b被其他線程引用
//因此 sb 對(duì)象屬于不可能共享的資源,JVM 會(huì)自動(dòng)消除 sb.append() 方法的鎖
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 100; i++) {
withoutSync.add("a", "b");
}
}
}
鎖粗化
通過(guò)擴(kuò)大加鎖的范圍,避免反復(fù)加鎖和釋放鎖
public class CoarseSync {
public static String copyString100Times(String target) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
//由于 append 方法是 synchronized 的。每次 append 都會(huì)去申請(qǐng)鎖,JVM 檢測(cè)到在循環(huán)中的加鎖和釋放鎖操作,
// 比較耗時(shí),就會(huì)將鎖粗化到循環(huán)外部,只加一次鎖,這樣就提高了性能
sb.append(target);
}
return sb.toString();
}
}
在上面代碼中,由于 append 方法是 synchronized 的。每次 append 都會(huì)去申請(qǐng)鎖,JVM 檢測(cè)到在循環(huán)中的加鎖和釋放鎖操作,比較耗時(shí),就會(huì)將鎖粗化到循環(huán)外部,只加一次鎖,這樣就提高了性能。
synchronized 的四種狀態(tài)
無(wú)鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖。
會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí),鎖膨脹的方向?yàn)椋簾o(wú)鎖 -> 偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖。
在特定情況下還會(huì)進(jìn)行鎖降級(jí),當(dāng) JVM 運(yùn)行到安全點(diǎn)(Safe Point)時(shí),會(huì)檢查是否有空閑的Monitor,并嘗試將其進(jìn)行降級(jí)。
無(wú)鎖就對(duì)應(yīng)上面的鎖消除,重量鎖就是Monitor,下面重點(diǎn)說(shuō)一下偏向鎖和輕量級(jí)鎖。
偏向鎖的出現(xiàn)是為了減少同一線程獲取鎖的代價(jià)。
在大多數(shù)情況下,鎖不存在多線程競(jìng)爭(zhēng),總是由同一線程多次獲得。為了減少同一線程獲取鎖的代價(jià),引入了偏向鎖。
偏向鎖的核心思想是:如果一個(gè)線程獲得了鎖,那么鎖進(jìn)入偏向模式,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)該線程再次清秋鎖時(shí),無(wú)需再做任何同步操作,即獲取鎖的過(guò)程只需要檢查 Mark Word 的鎖標(biāo)記位,以及當(dāng)前線程 ID 等于 Mark Word 的 ThreadID即可,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作。
當(dāng)一個(gè)線程訪問(wèn)同步塊并獲取到鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程 ID,以后該線程再進(jìn)入和推出同步塊時(shí),不需要進(jìn)行 CAS 操作來(lái)加鎖和解鎖,從而提高了程序的性能。
對(duì)于沒(méi)有鎖競(jìng)爭(zhēng)的場(chǎng)合,偏向鎖有很好的優(yōu)化效果。但是對(duì)于鎖競(jìng)爭(zhēng)比較激烈的多線程場(chǎng)合,偏向鎖就失去作用了,這時(shí)偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖。
輕量級(jí)鎖
輕量級(jí)鎖是由偏向鎖升級(jí)來(lái)的,偏向鎖運(yùn)行在一個(gè)線程進(jìn)入同步塊的情況下,當(dāng)?shù)诙€(gè)線程加入鎖競(jìng)爭(zhēng)的時(shí)候,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖。
輕量級(jí)鎖適用的場(chǎng)景是線程交替執(zhí)行同步塊的情況,若存在同一時(shí)間訪問(wèn)同一鎖的情況,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。
在線程進(jìn)入同步代碼塊時(shí),如果同步對(duì)象鎖狀態(tài)為無(wú)鎖狀態(tài)(鎖標(biāo)志位為"01"狀態(tài)),虛擬機(jī)首先在當(dāng)當(dāng)前線程的棧幀中建立一個(gè)名為瑣記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝,官方稱之為 Displaced Mark Word,這時(shí)線程堆棧與對(duì)象頭的狀態(tài)如圖所示:
<div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/6/1714e3c8c16eb1b6?w=846&h=445&f=png&s=7226"/> </div>把對(duì)象頭中的 Mark Word 拷貝到棧幀的瑣記錄中
拷貝成功后,虛擬機(jī)將使用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針,并將 Lock Record 里的 owner 指針指向?qū)ο蟮?Mark Word,如果更次呢成功,則執(zhí)行步驟 4,否則執(zhí)行步驟 5
如果步驟 3 中的更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象 Mark Word 的鎖標(biāo)志位設(shè)置為"00",表示該對(duì)象處于輕量鎖狀態(tài)。這時(shí)候線程棧幀與對(duì)象頭的狀態(tài)如圖所示:
<div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/6/1714e3c8c1c26102?w=849&h=477&f=png&s=27759"/> </div>如果步驟 3 的更新操作失敗了,虛擬機(jī)會(huì)首先檢查對(duì)象的 Mark Word 的是否指向當(dāng)前線程的棧幀,如果時(shí)就說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說(shuō)明多個(gè)線程在競(jìng)爭(zhēng)鎖,輕量級(jí)鎖膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)變?yōu)?10",Mark Word 中存儲(chǔ)的就變?yōu)橹赶蛑亓考?jí)鎖的指針,當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖,而后面等待鎖的線程要進(jìn)入阻塞狀態(tài)。
下面再來(lái)講講輕量級(jí)鎖解鎖的過(guò)程。
解鎖的過(guò)程:
- 通過(guò) CAS 操作嘗試把線程棧幀中復(fù)制的Displaced Mark Word 替換到對(duì)象當(dāng)前的 Mark Word 中
- 如果替換成功,整個(gè)同步過(guò)程就完成了
- 如果替換失敗,說(shuō)明由其他線程嘗試獲取該鎖(此時(shí)鎖已經(jīng)膨脹為重量級(jí)鎖),那就要在釋放鎖的同時(shí),喚醒被掛起的線程
這里需要說(shuō)明一下為什么把棧幀中的 Displaced Mark Word 成功替換到對(duì)象的 Mark Word 中,就是解鎖成功了?這里需要從鎖的內(nèi)存語(yǔ)義來(lái)理解
鎖的內(nèi)存語(yǔ)義
- 當(dāng)線程釋放鎖時(shí),Java 內(nèi)存模型會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中
- 當(dāng)線程獲取鎖時(shí),Java 內(nèi)存模型會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效,從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量
也就是說(shuō)線程 A 釋放一個(gè)鎖,實(shí)際上是線程 A 向接下來(lái)將要獲取這個(gè)鎖的某個(gè)線程發(fā)出消息,這個(gè)消息就是線程 A 對(duì)共享變量所做的修改。
線程 B 獲取某個(gè)鎖,實(shí)際上是線程 B 接收之前占有這個(gè)鎖的線程發(fā)出的消息,這個(gè)消息就是在釋放鎖之前對(duì)共享變量所做的修改。
| 鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 使用場(chǎng)景 |
|---|---|---|---|
| 偏向鎖 | 加鎖和解鎖不需要 CAS 操作,沒(méi)有額外的性能消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差別 | 如果線程之間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷的消耗 | 只有一個(gè)線程訪問(wèn)同步代碼塊或者同步方法的場(chǎng)景 |
| 輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了相應(yīng)速度 | 如果線程長(zhǎng)時(shí)間搶占不到鎖,自旋會(huì)消耗 CPU 性能 | 線程交替執(zhí)行方法同步塊或者同步方法的場(chǎng)景 |
| 重量級(jí)鎖 | 線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗 CPU | 線程阻塞,響應(yīng)時(shí)間緩慢,在多線程下,頻繁地獲取和釋放鎖,會(huì)帶來(lái)巨大的性能消耗 | 追求吞吐量,同步代碼塊或者同步方法執(zhí)行時(shí)間較長(zhǎng)的場(chǎng)景 |
如果你覺(jué)得這篇文章對(duì)你有幫助,不妨點(diǎn)個(gè)贊,讓我有更多動(dòng)力寫(xiě)出好文章。
我的文章會(huì)首發(fā)在公眾號(hào)上,歡迎掃碼關(guān)注我的公眾號(hào)張賢同學(xué)。
