這篇文章主要講解了“怎么理解synchronized與鎖的關(guān)系”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(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é)碼會(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í)鎖
感謝各位的閱讀,以上就是“怎么理解synchronized與鎖的關(guān)系”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)怎么理解synchronized與鎖的關(guān)系這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
文章名稱:怎么理解synchronized與鎖的關(guān)系
分享路徑:http://aaarwkj.com/article16/pjdhgg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供微信公眾號(hào)、品牌網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、ChatGPT、定制開發(fā)、Google
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)