這回,要將 synchronized 與鎖的關(guān)系講透?。?!
說到鎖,都會(huì)提 synchronized 。這個(gè)英文單詞兒啥意思呢?翻譯成中文就是「同步」的意思
一般都是使用 synchronized 這個(gè)關(guān)鍵字來給一段代碼或者一個(gè)方法上鎖,使得這段代碼或者方法,在同一個(gè)時(shí)刻只能有一個(gè)線程來執(zhí)行它。
synchronized 相比于 volatile 來說,用的比較靈活,你可以在方法上使用,可以在靜態(tài)方法上使用,也可以在代碼塊上使用。
關(guān)于 synchronized 這一塊大概就說到這里,阿粉今天想著重來說一下, synchronized 底層是怎么實(shí)現(xiàn)的
JVM 是如何實(shí)現(xiàn) synchronized 的?
我知道可以利用 synchronized 關(guān)鍵字來給程序進(jìn)行加鎖,但是它具體怎么實(shí)現(xiàn)的我不清楚呀,別急,咱們先來看個(gè) demo :
- public class demo {
 - public void synchronizedDemo(Object lock){
 - synchronized(lock){
 - lock.hashCode();
 - }
 - }
 - }
 
上面是我寫的一個(gè) demo ,然后進(jìn)入到 class 文件所在的目錄下,使用 javap -v demo.class 來看一下編譯的字節(jié)碼(在這里我截取了一部分):
- public void synchronizedDemo(java.lang.Object);
 - descriptor: (Ljava/lang/Object;)V
 - flags: ACC_PUBLIC
 - Code:
 - stack=2, locals=4, args_size=2
 - 0: aload_1
 - 1: dup
 - 2: astore_2
 - 3: monitorenter
 - 4: aload_1
 - 5: invokevirtual #2 // Method java/lang/Object.hashCode:()I
 - 8: pop
 - 9: aload_2
 - 10: monitorexit
 - 11: goto 19
 - 14: astore_3
 - 15: aload_2
 - 16: monitorexit
 - 17: aload_3
 - 18: athrow
 - 19: return
 - Exception table:
 - from to target type
 - 4 11 14 any
 - 14 17 14 any
 
應(yīng)該能夠看到當(dāng)程序聲明 synchronized 代碼塊時(shí),編譯成的字節(jié)碼會(huì)包含 monitorenter和 monitorexit 指令,這兩種指令會(huì)消耗操作數(shù)棧上的一個(gè)引用類型的元素(也就是 synchronized 關(guān)鍵字括號(hào)里面的引用),作為所要加鎖解鎖的鎖對(duì)象。如果看的比較仔細(xì)的話,上面有一個(gè) monitorenter 指令和兩個(gè) monitorexit 指令,這是 Java 虛擬機(jī)為了確保獲得的鎖不管是在正常執(zhí)行路徑,還是在異常執(zhí)行路徑上都能夠解鎖。
- 關(guān)于 monitorenter 和 monitorexit ,可以理解為每個(gè)鎖對(duì)象擁有一個(gè)鎖計(jì)數(shù)器和一個(gè)指向持有該鎖的線程指針:
 - 當(dāng)程序執(zhí)行 monitorenter 時(shí),如果目標(biāo)鎖對(duì)象的計(jì)數(shù)器為 0 ,說明這個(gè)時(shí)候它沒有被其他線程所占有,此時(shí)如果有線程來請(qǐng)求使用, Java 虛擬機(jī)就會(huì)分配給該線程,并且把計(jì)數(shù)器的值加 1
 - 目標(biāo)鎖對(duì)象計(jì)數(shù)器不為 0 時(shí),如果鎖對(duì)象持有的線程是當(dāng)前線程, Java 虛擬機(jī)可以將其計(jì)數(shù)器加 1 ,如果不是呢?那很抱歉,就只能等待,等待持有線程釋放掉
 
當(dāng)執(zhí)行 monitorexit 時(shí), Java 虛擬機(jī)就將鎖對(duì)象的計(jì)數(shù)器減 1 ,當(dāng)計(jì)數(shù)器減到 0 時(shí),說明這個(gè)鎖就被釋放掉了,此時(shí)如果有其他線程來請(qǐng)求,就可以請(qǐng)求成功
為什么采用這種方式呢?是為了允許同一個(gè)線程重復(fù)獲取同一把鎖。比如,一個(gè) Java 類中擁有好多個(gè) synchronized 方法,那這些方法之間的相互調(diào)用,不管是直接的還是間接的,都會(huì)涉及到對(duì)同一把鎖的重復(fù)加鎖操作。這樣去設(shè)計(jì)的話,就可以避免這種情況。
鎖
在 Java 多線程中,所有的鎖都是基于對(duì)象的。也就是說, Java 中的每一個(gè)對(duì)象都可以作為一個(gè)鎖。你可能會(huì)有疑惑,不對(duì)呀,不是還有類鎖嘛。但是 class 對(duì)象也是特殊的 Java 對(duì)象,所以呢,在 Java 中所有的鎖都是基于對(duì)象的
在 Java6 之前,所有的鎖都是"重量級(jí)"鎖,重量級(jí)鎖會(huì)帶來一個(gè)問題,就是如果程序頻繁獲得鎖釋放鎖,就會(huì)導(dǎo)致性能的極大消耗。為了優(yōu)化這個(gè)問題,引入了"偏向鎖"和"輕量級(jí)鎖"的概念。所以在 Java6 及其以后的版本,一個(gè)對(duì)象有 4 種鎖狀態(tài):無鎖狀態(tài),偏向鎖狀態(tài),輕量級(jí)鎖狀態(tài),重量級(jí)鎖狀態(tài)。
在 4 種鎖狀態(tài)中,無鎖狀態(tài)應(yīng)該比較好理解,無鎖就是沒有鎖,任何線程都可以嘗試修改,所以這里就一筆帶過了。
隨著競(jìng)爭(zhēng)情況的出現(xiàn),鎖的升級(jí)非常容易發(fā)生,但是如果想要讓鎖降級(jí),條件非常苛刻,有種你想來可以,但是想走不行的趕腳。
阿粉在這里啰嗦一句:很多文章說,鎖如果升級(jí)之后是不能降級(jí)的,其實(shí)在 HotSpot JVM 中,是支持鎖降級(jí)的
鎖降級(jí)發(fā)生在 Stop The World 期間,當(dāng) JVM 進(jìn)入安全點(diǎn)的時(shí)候,會(huì)檢查有沒有閑置的鎖,如果有就會(huì)嘗試進(jìn)行降級(jí)
看到 Stop The World 和 安全點(diǎn) 可能有人比較懵,我這里簡(jiǎn)單說一下,具體還需要讀者自己去探索一番.(因?yàn)檫@是 JVM 的內(nèi)容,這篇文章的重點(diǎn)不是 JVM )
在 Java 虛擬機(jī)里面,傳統(tǒng)的垃圾回收算法采用的是一種簡(jiǎn)單粗暴的方式,就是 Stop-the-world ,而這個(gè) Stop-the-world 就是通過安全點(diǎn)( safepoint )機(jī)制來實(shí)現(xiàn)的,安全點(diǎn)是什么意思呢?就是 Java 程序在執(zhí)行本地代碼時(shí),如果這段代碼不訪問 Java 對(duì)象/調(diào)用 Java 方法/返回到原來的 Java 方法,那 Java 虛擬機(jī)的堆棧就不會(huì)發(fā)生改變,這就代表執(zhí)行的這段本地代碼可以作為一個(gè)安全點(diǎn)。當(dāng) Java 虛擬機(jī)收到 Stop-the-world 請(qǐng)求時(shí),它會(huì)等所有的線程都到達(dá)安全點(diǎn)之后,才允許請(qǐng)求 Stop-the-world 的線程進(jìn)行獨(dú)占工作
接下來就介紹一下幾種鎖和鎖升級(jí)
Java 對(duì)象頭
在剛開始就說了, Java 的鎖都是基于對(duì)象的,那是怎么告訴程序我是個(gè)鎖呢?就不得不來說, Java 對(duì)象頭 每個(gè) Java 對(duì)象都有對(duì)象頭,如果是非數(shù)組類型,就用 2 個(gè)字寬來存儲(chǔ)對(duì)象頭,如果是數(shù)組,就用 3 個(gè)字寬來存儲(chǔ)對(duì)象頭。在 32 位處理器中,一個(gè)字寬是 32 位;在 64 位處理器中,字寬就是 64 位咯~對(duì)象頭的內(nèi)容就是下面這樣:
| 長(zhǎng)度 | 內(nèi)容 | 說明 | 
|---|---|---|
| 32/64 bit | Mark Word | 存儲(chǔ)對(duì)象的 hashCode 或鎖信息等 | 
| 32/64 bit | Class Metadata Address | 存儲(chǔ)到對(duì)象類型數(shù)據(jù)的指針 | 
| 32/64 bit | Array length | 數(shù)組的長(zhǎng)度(如果是數(shù)組) | 
        
咱們主要來看 Mark Word 的內(nèi)容:
| 鎖狀態(tài) | 29 bit/61 bit | 1 bit 是否是偏向鎖 | 2 bit 鎖標(biāo)志位 | 
|---|---|---|---|
| 無鎖 | 0 | 01 | |
| 偏向鎖 | 線程 ID | 1 | 01 | 
| 輕量級(jí)鎖 | 指向棧中鎖記錄的指針 | 此時(shí)這一位不用于標(biāo)識(shí)偏向鎖 | 00 | 
| 重量級(jí)鎖 | 指向互斥量(重量級(jí)鎖)的指針 | 此時(shí)這一位不用于標(biāo)識(shí)偏向鎖 | 10 | 
| GC 標(biāo)記 | 此時(shí)這一位不用于標(biāo)識(shí)偏向鎖 | 11 | 
        
從上面表格中,應(yīng)該能夠看到,是偏向鎖時(shí), Mark Word 存儲(chǔ)的是偏向鎖的線程 ID ;是輕量級(jí)鎖時(shí), Mark Word 存儲(chǔ)的是指向線程棧中 Lock Record 的指針;是重量級(jí)鎖時(shí), Mark Word 存儲(chǔ)的是指向堆中的 monitor 對(duì)象的指針
偏向鎖
HotSpot 的作者經(jīng)過大量的研究發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得
基于此,就引入了偏向鎖的概念
所以啥是偏向鎖呢?用大白話說就是,我現(xiàn)在給鎖設(shè)置一個(gè)變量,當(dāng)一個(gè)線程請(qǐng)求的時(shí)候,發(fā)現(xiàn)這個(gè)鎖是 true ,也就是說這個(gè)時(shí)候沒有所謂的資源競(jìng)爭(zhēng),那也不用走什么加鎖/解鎖的流程了,直接拿來用就行。但是如果這個(gè)鎖是 false 的話,說明存在其他線程競(jìng)爭(zhēng)資源,那咱們?cè)僮哒?guī)的流程
看一下具體的實(shí)現(xiàn)原理:
當(dāng)一個(gè)線程第一次進(jìn)入同步塊時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄中存儲(chǔ)鎖偏向的線程 ID 。當(dāng)下次該線程進(jìn)入這個(gè)同步塊時(shí),會(huì)檢查鎖的 Mark Word 里面存放的是不是自己的線程 ID。如果是,說明線程已經(jīng)獲得了鎖,那么這個(gè)線程在進(jìn)入和退出同步塊時(shí),都不需要花費(fèi) CAS 操作來加鎖和解鎖;如果不是,說明有另外一個(gè)線程來競(jìng)爭(zhēng)這個(gè)偏向鎖,這時(shí)就會(huì)嘗試使用 CAS 來替換 Mark Word 里面的線程 ID 為新線程的 ID 。此時(shí)會(huì)有兩種情況:
- 替換成功,說明之前的線程不存在了,那么 Mark Word 里面的線程 ID 為新線程的 ID ,鎖不會(huì)升級(jí),此時(shí)仍然為偏向鎖
 - 替換失敗,說明之前的線程仍然存在,那就暫停之前的線程,設(shè)置偏向鎖標(biāo)識(shí)為 0 ,并設(shè)置鎖標(biāo)志位為 00 ,升級(jí)為輕量級(jí)鎖,按照輕量級(jí)鎖的方式進(jìn)行競(jìng)爭(zhēng)鎖
 
撤銷偏向鎖
偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)時(shí)才釋放鎖的機(jī)制。也就說,如果沒有人來和我競(jìng)爭(zhēng)鎖的時(shí)候,那么這個(gè)鎖就是我獨(dú)有的,當(dāng)其他線程嘗試和我競(jìng)爭(zhēng)偏向鎖時(shí),我會(huì)釋放這個(gè)鎖
在偏向鎖向輕量級(jí)鎖升級(jí)時(shí),首先會(huì)暫停擁有偏向鎖的線程,重置偏向鎖標(biāo)識(shí),看起來這個(gè)過程挺簡(jiǎn)單的,但是開銷是很大的,因?yàn)?
- 首先需要在一個(gè)安全點(diǎn)停止擁有鎖的線程
 - 然后遍歷線程棧,如果存在鎖記錄的話,就需要修復(fù)鎖記錄和 Mark Word ,變成無鎖狀態(tài)
 - 最后喚醒被停止的線程,把偏向鎖升級(jí)成輕量級(jí)鎖
 
你以為就是升級(jí)一個(gè)輕量級(jí)鎖?too young too simple
偏向鎖向輕量級(jí)鎖升級(jí)的過程中,是非常耗費(fèi)資源的,如果應(yīng)用程序中所有的鎖通常都處于競(jìng)爭(zhēng)狀態(tài),偏向鎖此時(shí)就是一個(gè)累贅,此時(shí)就可以通過 JVM 參數(shù)關(guān)閉偏向鎖: -XX:-UseBiasedLocking=false ,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)
最后,來張圖吧~
輕量級(jí)鎖
如果多個(gè)線程在不同時(shí)段獲取同一把鎖,也就是不存在鎖競(jìng)爭(zhēng)的情況,那么 JVM 就會(huì)使用輕量級(jí)鎖來避免線程的阻塞與喚醒
輕量級(jí)鎖加鎖
JVM 會(huì)為每個(gè)線程在當(dāng)前線程的棧幀中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,稱之為 Displaced Mark Word 。如果一個(gè)線程獲得鎖的時(shí)候發(fā)現(xiàn)是輕量級(jí)鎖,就會(huì)將鎖的 Mark Word 復(fù)制到自己的 Displaced Mark Word 中。之后線程會(huì)嘗試用 CAS 將鎖的 Mark Word 替換為指向鎖記錄的指針。
如果替換成功,當(dāng)前線程獲得鎖,那么整個(gè)狀態(tài)還是 輕量級(jí)鎖 狀態(tài)
如果替換失敗了呢?說明 Mark Word 被替換成了其他線程的鎖記錄,那就嘗試使用自旋來獲取鎖.(自旋是說,線程不斷地去嘗試獲取鎖,一般都是用循環(huán)來實(shí)現(xiàn)的)
自旋是耗費(fèi) CPU 的,如果一直獲取不到鎖,線程就會(huì)一直自旋, CPU 那么寶貴的資源就這么被白白浪費(fèi)了
解決這個(gè)問題最簡(jiǎn)單的辦法就是指定自旋的次數(shù),比如如果沒有替換成功,那就循環(huán) 10 次,還沒有獲取到,那就進(jìn)入阻塞狀態(tài)
但是 JDK 采用了一個(gè)更加巧妙的方法---適應(yīng)性自旋。就是說,如果這次線程自旋成功了,那我下次自旋次數(shù)更多一些,因?yàn)槲疫@次自旋成功,說明我成功的概率還是挺大的,下次自旋次數(shù)就更多一些,那么如果自旋失敗了,下次我自旋次數(shù)就減少一些,就比如,已經(jīng)看到了失敗的前兆,那我就先溜,而不是非要“不撞南墻不回頭”
自旋失敗之后,線程就會(huì)阻塞,同時(shí)鎖會(huì)升級(jí)成重量級(jí)鎖
輕量級(jí)鎖釋放:
在釋放鎖時(shí),當(dāng)前線程會(huì)使用 CAS 操作將 Displaced Mark Word 中的內(nèi)容復(fù)制到鎖的 Mark Word 里面。如果沒有發(fā)生競(jìng)爭(zhēng),這個(gè)復(fù)制的操作就會(huì)成功;如果有其他線程因?yàn)樽孕啻螌?dǎo)致輕量級(jí)鎖升級(jí)成了重量級(jí)鎖, CAS 操作就會(huì)失敗,此時(shí)會(huì)釋放鎖同時(shí)喚醒被阻塞的過程
同樣,來一張圖吧:
重量級(jí)鎖
重量級(jí)鎖依賴于操作系統(tǒng)的互斥量( mutex )來實(shí)現(xiàn)。但是操作系統(tǒng)中線程間狀態(tài)的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間(因?yàn)椴僮飨到y(tǒng)需要從用戶態(tài)切換到內(nèi)核態(tài),這個(gè)切換成本很高),所以重量級(jí)鎖效率很低,但是有一點(diǎn)就是,被阻塞的線程是不會(huì)消耗 CPU 的
每一個(gè)對(duì)象都可以當(dāng)做一個(gè)鎖,那么當(dāng)多個(gè)線程同時(shí)請(qǐng)求某個(gè)對(duì)象鎖時(shí),它會(huì)怎么處理呢?
對(duì)象鎖會(huì)設(shè)置集中狀態(tài)來區(qū)分請(qǐng)求的線程:
Contention List:所有請(qǐng)求鎖的線程將被首先放置到該競(jìng)爭(zhēng)隊(duì)列
Entry List: Contention List 中那些有資格成為候選人的線程被移到 Entry List 中
Wait Set:調(diào)用 wait 方法被阻塞的線程會(huì)被放置到 Wait Set 中
OnDeck:任何時(shí)刻最多只能有一個(gè)線程正在競(jìng)爭(zhēng)鎖,該線程稱為 OnDeck
Owner:獲得鎖的線程稱為 Owner
!Owner:釋放鎖的線程
當(dāng)一個(gè)線程嘗試獲得鎖時(shí),如果這個(gè)鎖被占用,就會(huì)把該線程封裝成一個(gè) ObjectWaiter對(duì)象插入到 Contention List 隊(duì)列的隊(duì)首,然后調(diào)用 park 函數(shù)掛起當(dāng)前線程
當(dāng)線程釋放鎖時(shí),會(huì)從 Contention List 或者 Entry List 中挑選一個(gè)線程進(jìn)行喚醒
如果線程在獲得鎖之后,調(diào)用了 Object.wait 方法,就會(huì)將該線程放入到 WaitSet 中,當(dāng)被 Object.notify 喚醒后,會(huì)將線程從 WaitSet 移動(dòng)到 Contention List 或者 Entry List 中。
但是,當(dāng)調(diào)用一個(gè)鎖對(duì)象的 wait 或 notify 方法時(shí),如果當(dāng)前鎖的狀態(tài)是偏向鎖或輕量級(jí)鎖,則會(huì)先膨脹成重量級(jí)鎖
總結(jié):
synchronized 關(guān)鍵字是通過 monitorenter 和 monitorexit 兩種指令來保證鎖的
當(dāng)一個(gè)線程準(zhǔn)備獲取共享資源時(shí):
- 首先檢查 MarkWord 里面放的是不是自己的 ThreadID ,如果是,說明當(dāng)前線程處于 "偏向鎖"
 - 如果不是,鎖升級(jí),這時(shí)使用 CAS 操作來執(zhí)行切換,新的線程根據(jù) MarkWord 里面現(xiàn)有的 ThreadID 來通知之前的線程暫停,將 MarkWord 的內(nèi)容置為空
 - 然后,兩個(gè)線程都將鎖對(duì)象 HashCode 復(fù)制到自己新建的用于存儲(chǔ)鎖的記錄空間中,接著開始通過 CAS 操作,把鎖對(duì)象的 MarkWord 的內(nèi)容修改為自己新建的記錄空間地址,以這種方式競(jìng)爭(zhēng) MarkWord ,成功執(zhí)行 CAS 的線程獲得資源,失敗的則進(jìn)入自旋
 - 自旋的線程在自旋過程中,如果成功獲得資源(也就是之前獲得資源的線程執(zhí)行完畢,釋放了共享資源),那么整個(gè)狀態(tài)依然是 輕量級(jí)鎖 的狀態(tài)
 - 如果沒有獲得資源,就進(jìn)入 重量級(jí)鎖 的狀態(tài),此時(shí),自旋的線程進(jìn)行阻塞,等待之前線程執(zhí)行完成并且喚醒自己
 
參考:
- Java 并發(fā)編程的技術(shù)
 - 極客時(shí)間---深入拆解 Java 虛擬機(jī)
 
到這里,整篇文章的內(nèi)容就算是結(jié)束了。
沒想到這篇文章竟然被阿粉寫了有 5000 多字(阿粉不會(huì)告訴你這篇文章,是阿粉在假期里面從大早上八點(diǎn)就開始寫,寫到下午五六點(diǎn),累到虛脫的我
能夠閱讀到這里的各位,都是最靚的仔仔

















 
 
 













 
 
 
 