這篇文章主要講解了“怎么理解synchronized與鎖的關(guān)系”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“怎么理解synchronized與鎖的關(guān)系”吧!
網(wǎng)站建設(shè)哪家好,找成都創(chuàng)新互聯(lián)!專注于網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、成都小程序開發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了彌渡免費(fèi)建站歡迎大家使用!
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é)碼會包含 monitorenter和 monitorexit 指令,這兩種指令會消耗操作數(shù)棧上的一個(gè)引用類型的元素(也就是 synchronized 關(guān)鍵字括號里面的引用),作為所要加鎖解鎖的鎖對象。如果看的比較仔細(xì)的話,上面有一個(gè) monitorenter 指令和兩個(gè) monitorexit 指令,這是 Java 虛擬機(jī)為了確保獲得的鎖不管是在正常執(zhí)行路徑,還是在異常執(zhí)行路徑上都能夠解鎖。
關(guān)于 monitorenter 和 monitorexit ,可以理解為每個(gè)鎖對象擁有一個(gè)鎖計(jì)數(shù)器和一個(gè)指向持有該鎖的線程指針:
當(dāng)程序執(zhí)行 monitorenter 時(shí),如果目標(biāo)鎖對象的計(jì)數(shù)器為 0 ,說明這個(gè)時(shí)候它沒有被其他線程所占有,此時(shí)如果有線程來請求使用, Java 虛擬機(jī)就會分配給該線程,并且把計(jì)數(shù)器的值加 1
目標(biāo)鎖對象計(jì)數(shù)器不為 0 時(shí),如果鎖對象持有的線程是當(dāng)前線程, Java 虛擬機(jī)可以將其計(jì)數(shù)器加 1 ,如果不是呢?那很抱歉,就只能等待,等待持有線程釋放掉
當(dāng)執(zhí)行 monitorexit 時(shí), Java 虛擬機(jī)就將鎖對象的計(jì)數(shù)器減 1 ,當(dāng)計(jì)數(shù)器減到 0 時(shí),說明這個(gè)鎖就被釋放掉了,此時(shí)如果有其他線程來請求,就可以請求成功
為什么采用這種方式呢?是為了允許同一個(gè)線程重復(fù)獲取同一把鎖。比如,一個(gè) Java 類中擁有好多個(gè) synchronized 方法,那這些方法之間的相互調(diào)用,不管是直接的還是間接的,都會涉及到對同一把鎖的重復(fù)加鎖操作。這樣去設(shè)計(jì)的話,就可以避免這種情況。
鎖
在 Java 多線程中,所有的鎖都是基于對象的。也就是說, Java 中的每一個(gè)對象都可以作為一個(gè)鎖。你可能會有疑惑,不對呀,不是還有類鎖嘛。但是 class 對象也是特殊的 Java 對象,所以呢,在 Java 中所有的鎖都是基于對象的
在 Java6 之前,所有的鎖都是"重量級"鎖,重量級鎖會帶來一個(gè)問題,就是如果程序頻繁獲得鎖釋放鎖,就會導(dǎo)致性能的極大消耗。為了優(yōu)化這個(gè)問題,引入了"偏向鎖"和"輕量級鎖"的概念。所以在 Java6 及其以后的版本,一個(gè)對象有 4 種鎖狀態(tài):無鎖狀態(tài),偏向鎖狀態(tài),輕量級鎖狀態(tài),重量級鎖狀態(tài)。
在 4 種鎖狀態(tài)中,無鎖狀態(tài)應(yīng)該比較好理解,無鎖就是沒有鎖,任何線程都可以嘗試修改,所以這里就一筆帶過了。
隨著競爭情況的出現(xiàn),鎖的升級非常容易發(fā)生,但是如果想要讓鎖降級,條件非??量?,有種你想來可以,但是想走不行的趕腳。
阿粉在這里啰嗦一句:很多文章說,鎖如果升級之后是不能降級的,其實(shí)在 HotSpot JVM 中,是支持鎖降級的
鎖降級發(fā)生在 Stop The World 期間,當(dāng) JVM 進(jìn)入安全點(diǎn)的時(shí)候,會檢查有沒有閑置的鎖,如果有就會嘗試進(jìn)行降級
看到 Stop The World 和 安全點(diǎn) 可能有人比較懵,我這里簡單說一下,具體還需要讀者自己去探索一番.(因?yàn)檫@是 JVM 的內(nèi)容,這篇文章的重點(diǎn)不是 JVM )
在 Java 虛擬機(jī)里面,傳統(tǒng)的垃圾回收算法采用的是一種簡單粗暴的方式,就是 Stop-the-world ,而這個(gè) Stop-the-world 就是通過安全點(diǎn)( safepoint )機(jī)制來實(shí)現(xiàn)的,安全點(diǎn)是什么意思呢?就是 Java 程序在執(zhí)行本地代碼時(shí),如果這段代碼不訪問 Java 對象/調(diào)用 Java 方法/返回到原來的 Java 方法,那 Java 虛擬機(jī)的堆棧就不會發(fā)生改變,這就代表執(zhí)行的這段本地代碼可以作為一個(gè)安全點(diǎn)。當(dāng) Java 虛擬機(jī)收到 Stop-the-world 請求時(shí),它會等所有的線程都到達(dá)安全點(diǎn)之后,才允許請求 Stop-the-world 的線程進(jìn)行獨(dú)占工作
接下來就介紹一下幾種鎖和鎖升級
Java 對象頭
在剛開始就說了, Java 的鎖都是基于對象的,那是怎么告訴程序我是個(gè)鎖呢?就不得不來說, Java 對象頭 每個(gè) Java 對象都有對象頭,如果是非數(shù)組類型,就用 2 個(gè)字寬來存儲對象頭,如果是數(shù)組,就用 3 個(gè)字寬來存儲對象頭。在 32 位處理器中,一個(gè)字寬是 32 位;在 64 位處理器中,字寬就是 64 位咯~對象頭的內(nèi)容就是下面這樣:
長度 | 內(nèi)容 | 說明 |
---|---|---|
32/64 bit | Mark Word | 存儲對象的 hashCode 或鎖信息等 |
32/64 bit | Class Metadata Address | 存儲到對象類型數(shù)據(jù)的指針 |
32/64 bit | Array length | 數(shù)組的長度(如果是數(shù)組) |
咱們主要來看 Mark Word 的內(nèi)容:
鎖狀態(tài) | 29 bit/61 bit | 1 bit 是否是偏向鎖 | 2 bit 鎖標(biāo)志位 |
---|---|---|---|
無鎖 | 0 | 01 | |
偏向鎖 | 線程 ID | 1 | 01 |
輕量級鎖 | 指向棧中鎖記錄的指針 | 此時(shí)這一位不用于標(biāo)識偏向鎖 | 00 |
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 此時(shí)這一位不用于標(biāo)識偏向鎖 | 10 |
GC 標(biāo)記 | 此時(shí)這一位不用于標(biāo)識偏向鎖 | 11 |
從上面表格中,應(yīng)該能夠看到,是偏向鎖時(shí), Mark Word 存儲的是偏向鎖的線程 ID ;是輕量級鎖時(shí), Mark Word 存儲的是指向線程棧中 Lock Record 的指針;是重量級鎖時(shí), Mark Word 存儲的是指向堆中的 monitor 對象的指針
偏向鎖
HotSpot 的作者經(jīng)過大量的研究發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得
基于此,就引入了偏向鎖的概念
所以啥是偏向鎖呢?用大白話說就是,我現(xiàn)在給鎖設(shè)置一個(gè)變量,當(dāng)一個(gè)線程請求的時(shí)候,發(fā)現(xiàn)這個(gè)鎖是 true ,也就是說這個(gè)時(shí)候沒有所謂的資源競爭,那也不用走什么加鎖/解鎖的流程了,直接拿來用就行。但是如果這個(gè)鎖是 false 的話,說明存在其他線程競爭資源,那咱們再走正規(guī)的流程
看一下具體的實(shí)現(xiàn)原理:
當(dāng)一個(gè)線程第一次進(jìn)入同步塊時(shí),會在對象頭和棧幀中的鎖記錄中存儲鎖偏向的線程 ID 。當(dāng)下次該線程進(jìn)入這個(gè)同步塊時(shí),會檢查鎖的 Mark Word 里面存放的是不是自己的線程 ID。如果是,說明線程已經(jīng)獲得了鎖,那么這個(gè)線程在進(jìn)入和退出同步塊時(shí),都不需要花費(fèi) CAS 操作來加鎖和解鎖;如果不是,說明有另外一個(gè)線程來競爭這個(gè)偏向鎖,這時(shí)就會嘗試使用 CAS 來替換 Mark Word 里面的線程 ID 為新線程的 ID 。此時(shí)會有兩種情況:
替換成功,說明之前的線程不存在了,那么 Mark Word 里面的線程 ID 為新線程的 ID ,鎖不會升級,此時(shí)仍然為偏向鎖
替換失敗,說明之前的線程仍然存在,那就暫停之前的線程,設(shè)置偏向鎖標(biāo)識為 0 ,并設(shè)置鎖標(biāo)志位為 00 ,升級為輕量級鎖,按照輕量級鎖的方式進(jìn)行競爭鎖
撤銷偏向鎖
偏向鎖使用了一種等到競爭出現(xiàn)時(shí)才釋放鎖的機(jī)制。也就說,如果沒有人來和我競爭鎖的時(shí)候,那么這個(gè)鎖就是我獨(dú)有的,當(dāng)其他線程嘗試和我競爭偏向鎖時(shí),我會釋放這個(gè)鎖
在偏向鎖向輕量級鎖升級時(shí),首先會暫停擁有偏向鎖的線程,重置偏向鎖標(biāo)識,看起來這個(gè)過程挺簡單的,但是開銷是很大的,因?yàn)?
首先需要在一個(gè)安全點(diǎn)停止擁有鎖的線程
然后遍歷線程棧,如果存在鎖記錄的話,就需要修復(fù)鎖記錄和 Mark Word ,變成無鎖狀態(tài)
最后喚醒被停止的線程,把偏向鎖升級成輕量級鎖
你以為就是升級一個(gè)輕量級鎖?too young too simple
偏向鎖向輕量級鎖升級的過程中,是非常耗費(fèi)資源的,如果應(yīng)用程序中所有的鎖通常都處于競爭狀態(tài),偏向鎖此時(shí)就是一個(gè)累贅,此時(shí)就可以通過 JVM 參數(shù)關(guān)閉偏向鎖: -XX:-UseBiasedLocking=false ,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)
最后,來張圖吧~
輕量級鎖
如果多個(gè)線程在不同時(shí)段獲取同一把鎖,也就是不存在鎖競爭的情況,那么 JVM 就會使用輕量級鎖來避免線程的阻塞與喚醒
輕量級鎖加鎖
JVM 會為每個(gè)線程在當(dāng)前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間,稱之為 Displaced Mark Word 。如果一個(gè)線程獲得鎖的時(shí)候發(fā)現(xiàn)是輕量級鎖,就會將鎖的 Mark Word 復(fù)制到自己的 Displaced Mark Word 中。之后線程會嘗試用 CAS 將鎖的 Mark Word 替換為指向鎖記錄的指針。
如果替換成功,當(dāng)前線程獲得鎖,那么整個(gè)狀態(tài)還是 輕量級鎖 狀態(tài)
如果替換失敗了呢?說明 Mark Word 被替換成了其他線程的鎖記錄,那就嘗試使用自旋來獲取鎖.(自旋是說,線程不斷地去嘗試獲取鎖,一般都是用循環(huán)來實(shí)現(xiàn)的)
自旋是耗費(fèi) CPU 的,如果一直獲取不到鎖,線程就會一直自旋, CPU 那么寶貴的資源就這么被白白浪費(fèi)了
解決這個(gè)問題最簡單的辦法就是指定自旋的次數(shù),比如如果沒有替換成功,那就循環(huán) 10 次,還沒有獲取到,那就進(jìn)入阻塞狀態(tài)
但是 JDK 采用了一個(gè)更加巧妙的方法---適應(yīng)性自旋。就是說,如果這次線程自旋成功了,那我下次自旋次數(shù)更多一些,因?yàn)槲疫@次自旋成功,說明我成功的概率還是挺大的,下次自旋次數(shù)就更多一些,那么如果自旋失敗了,下次我自旋次數(shù)就減少一些,就比如,已經(jīng)看到了失敗的前兆,那我就先溜,而不是非要“不撞南墻不回頭”
自旋失敗之后,線程就會阻塞,同時(shí)鎖會升級成重量級鎖
輕量級鎖釋放:
在釋放鎖時(shí),當(dāng)前線程會使用 CAS 操作將 Displaced Mark Word 中的內(nèi)容復(fù)制到鎖的 Mark Word 里面。如果沒有發(fā)生競爭,這個(gè)復(fù)制的操作就會成功;如果有其他線程因?yàn)樽孕啻螌?dǎo)致輕量級鎖升級成了重量級鎖, CAS 操作就會失敗,此時(shí)會釋放鎖同時(shí)喚醒被阻塞的過程
同樣,來一張圖吧:
重量級鎖
重量級鎖依賴于操作系統(tǒng)的互斥量( mutex )來實(shí)現(xiàn)。但是操作系統(tǒng)中線程間狀態(tài)的轉(zhuǎn)換需要相對比較長的時(shí)間(因?yàn)椴僮飨到y(tǒng)需要從用戶態(tài)切換到內(nèi)核態(tài),這個(gè)切換成本很高),所以重量級鎖效率很低,但是有一點(diǎn)就是,被阻塞的線程是不會消耗 CPU 的
每一個(gè)對象都可以當(dāng)做一個(gè)鎖,那么當(dāng)多個(gè)線程同時(shí)請求某個(gè)對象鎖時(shí),它會怎么處理呢?
對象鎖會設(shè)置集中狀態(tài)來區(qū)分請求的線程:
Contention List:所有請求鎖的線程將被首先放置到該競爭隊(duì)列
Entry List: Contention List 中那些有資格成為候選人的線程被移到 Entry List 中
Wait Set:調(diào)用 wait 方法被阻塞的線程會被放置到 Wait Set 中
OnDeck:任何時(shí)刻最多只能有一個(gè)線程正在競爭鎖,該線程稱為 OnDeck
Owner:獲得鎖的線程稱為 Owner
!Owner:釋放鎖的線程
當(dāng)一個(gè)線程嘗試獲得鎖時(shí),如果這個(gè)鎖被占用,就會把該線程封裝成一個(gè) ObjectWaiter對象插入到 Contention List 隊(duì)列的隊(duì)首,然后調(diào)用 park 函數(shù)掛起當(dāng)前線程
當(dāng)線程釋放鎖時(shí),會從 Contention List 或者 Entry List 中挑選一個(gè)線程進(jìn)行喚醒
如果線程在獲得鎖之后,調(diào)用了 Object.wait 方法,就會將該線程放入到 WaitSet 中,當(dāng)被 Object.notify 喚醒后,會將線程從 WaitSet 移動到 Contention List 或者 Entry List 中。
但是,當(dāng)調(diào)用一個(gè)鎖對象的 wait 或 notify 方法時(shí),如果當(dāng)前鎖的狀態(tài)是偏向鎖或輕量級鎖,則會先膨脹成重量級鎖
感謝各位的閱讀,以上就是“怎么理解synchronized與鎖的關(guān)系”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對怎么理解synchronized與鎖的關(guān)系這一問題有了更深刻的體會,具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識點(diǎn)的文章,歡迎關(guān)注!