動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

前言碎語(yǔ)

Synchronized和 ReentrantLock 大家應(yīng)該都不陌生了,作為java中最常用的本地鎖,最初版本中 ReentrantLock 的性能是遠(yuǎn)遠(yuǎn)強(qiáng)于 Synchronized 的,后續(xù)java在一次次的版本迭代中 對(duì) Synchronized 進(jìn)行了大量的優(yōu)化,直到 jdk1.6 之后,兩種鎖的性能已經(jīng)相差無(wú)幾,甚至 Synchronized 的自動(dòng)釋放鎖會(huì)更好用。

在面試時(shí)被問(wèn)到 Synchronized 和 ReentrantLock 的使用選擇時(shí),很多朋友都脫口而出的說(shuō)用 Synchronized ,甚至在我面試的時(shí)候問(wèn)面試者,也很少有人能夠答出所以然來(lái),moon 想說(shuō),這可不一定, 只對(duì)標(biāo)題感興趣的同學(xué)可以直接劃到最后 ,我可不是標(biāo)題黨~

Synchronized使用

在 java 代碼中 synchronized 的使用 是非常簡(jiǎn)單的

  • 1.直接貼在方法上(鎖住的是當(dāng)前類的字節(jié)碼文件)
  • 2.貼在代碼塊兒上(鎖住的是對(duì)象)
動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

程序運(yùn)行期間,Synchronized那一塊兒代碼發(fā)生么什么?

來(lái)看一張圖

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

在多線程運(yùn)行過(guò)程中, 線程會(huì)去先搶對(duì)象的監(jiān)視器 ,這個(gè)監(jiān)視器是對(duì)象獨(dú)有的,其實(shí)就相當(dāng)于一把鑰匙,搶到了,那你就獲得了當(dāng)前代碼塊兒的執(zhí)行權(quán)。

其他沒(méi)有搶到的線程會(huì)進(jìn)入隊(duì)列(SynchronizedQueue)當(dāng)中等待,等待當(dāng)前線程執(zhí)行完后,釋放鎖.

最后當(dāng)前線程執(zhí)行完畢后通知出隊(duì)然后繼續(xù)重復(fù)當(dāng)前過(guò)程.

從 jvm 的角度來(lái)看 monitorenter 和 monitorexit 指令代表著代碼的執(zhí)行與結(jié)束 。

SynchronizedQueue:

SynchronizedQueue 是一個(gè)比較特殊的隊(duì)列,它沒(méi)有存儲(chǔ)功能,它的功能就是維護(hù)一組線程,其中每個(gè)插入操作必須等待另一個(gè)線程的移除操作,同樣任何一個(gè)移除操作都等待另一個(gè)線程的插入操作。因此此隊(duì)列內(nèi)部其 實(shí)沒(méi)有任何一個(gè)元素,或者說(shuō)容量是0,嚴(yán)格說(shuō)并不是一種容器。由于隊(duì)列沒(méi)有容量,因此不能調(diào)用 peek 操作,因?yàn)橹挥幸瞥貢r(shí)才有元素。

舉個(gè)例子:

喝酒的時(shí)候, 先把酒倒入酒盅,然后再倒入酒杯,這就是正常的隊(duì)列 。

喝酒的時(shí)候, 把酒直接倒入酒杯,這就是 SynchronizedQueue

這個(gè)例子應(yīng)該很清晰易懂了,它的好處就是可以直接傳遞,省去了一個(gè)第三方傳遞的過(guò)程。

聊聊細(xì)節(jié),鎖升級(jí)的過(guò)程

在 jdk1.6 以前,Synchronized 是一個(gè)重量級(jí)鎖,還是先貼一張圖

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

這就是為什么說(shuō),Synchronized 是一個(gè)重量級(jí)鎖的原因, 因?yàn)槊恳淮捂i的資源都是直接和 cpu 去申請(qǐng)的,而 cpu 的鎖數(shù)量是固定的 ,當(dāng) cpu 鎖資源使用完后還會(huì)進(jìn)行鎖等待,這是一個(gè)非常耗時(shí)的操作。

但是在jdk1.6,針對(duì)代碼層面進(jìn)行了大量的優(yōu)化,也就是我們常說(shuō)的鎖升級(jí)的過(guò)程。

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

這就是一個(gè) 鎖升級(jí)的過(guò)程 ,我們簡(jiǎn)單的說(shuō)說(shuō):

  • 無(wú)鎖:對(duì)象一開(kāi)始就是無(wú)鎖狀態(tài)。
  • 偏向鎖: 相當(dāng)于給對(duì)象貼了一個(gè)標(biāo)簽 (將自己的線程 id 存入對(duì)象頭中),下次我再進(jìn)來(lái)時(shí),發(fā)現(xiàn)標(biāo)簽是我的,我就可以繼續(xù)使用了。
  • 自旋鎖: 想象一下有一個(gè)廁所,里面有一個(gè)人在,你很想上但是只有一個(gè)坑位,所以你只能徘徊等待,等那個(gè)人出來(lái)以后,你就可以使用了 。 這個(gè)自旋是使用 cas 來(lái)保證原子性的,關(guān)于 cas 我這里就不再贅述了。
  • 重量級(jí)鎖: 直接向 cpu 去申請(qǐng)申請(qǐng)鎖 ,其他的線程都進(jìn)入隊(duì)列中等待。

鎖升級(jí)是什么時(shí)候發(fā)生的?

  • 偏向鎖:一個(gè)線程獲取鎖時(shí)會(huì)由無(wú)鎖升級(jí)為偏向鎖
  • 自旋鎖:當(dāng)產(chǎn)生線程競(jìng)爭(zhēng)時(shí)由偏向鎖升級(jí)為自旋鎖,想象一下 while(true) ;
  • 重量級(jí)鎖:當(dāng)線程競(jìng)爭(zhēng)到達(dá)一定數(shù)量或超過(guò)一定時(shí)間時(shí),晉升為重量級(jí)鎖

鎖的信息是記錄在哪里的?

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

這張圖是對(duì)象頭中 markword 的數(shù)據(jù)結(jié)構(gòu) ,鎖的信息就是在這里存放的,很清楚的表明了鎖在升級(jí)的時(shí)候鎖信息的變動(dòng), 其實(shí)就是通過(guò)二進(jìn)制的數(shù)值,來(lái)對(duì)對(duì)象進(jìn)行一個(gè)標(biāo)記,每個(gè)數(shù)值代表一種狀態(tài) 。

既然synchronized有鎖升級(jí)那么有鎖降級(jí)嗎?

這個(gè)問(wèn)題和我們的題目就有很大的關(guān)聯(lián)了。

在 HotSpot 虛擬機(jī)中是有鎖降級(jí)的, 但是僅僅只發(fā)生在 STW 的時(shí)候 ,只有垃圾回收線程能夠觀測(cè)到它,也就是說(shuō), 在我們正常使用的過(guò)程中是不會(huì)發(fā)生鎖降級(jí)的,只有在 GC 的時(shí)候才會(huì)降級(jí)。

所以題目的答案,你懂了嗎?哈哈,我們接著往下走。

ReentrantLock的使用

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

ReentrantLock 的使用也是非常簡(jiǎn)單的,與 Synchronized 的不同就是需要自己去手動(dòng)釋放鎖,為了保證一定釋放,所以通常都是和 try~finally 配合使用的。

ReentrantLock的原理

ReentrantLock 意為 可重入鎖 ,說(shuō)起 ReentrantLock 就不得不說(shuō) AQS ,因?yàn)槠?底層就是使用 AQS 去實(shí)現(xiàn)的。

ReentrantLock有 兩種模式 ,一種是公平鎖,一種是非公平鎖。

  • 公平模式下等待線程入隊(duì)列后會(huì)嚴(yán)格按照隊(duì)列順序去執(zhí)行
  • 非公平模式下等待線程入隊(duì)列后有可能會(huì)出現(xiàn)插隊(duì)情況
動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

這就是ReentrantLock的結(jié)構(gòu)圖,我們看這張圖其實(shí)是很簡(jiǎn)單的,因?yàn)橹饕膶?shí)現(xiàn)都交給AQS去做了,我們下面著重聊一下AQS。

AQS

AQS(AbstractQueuedSynchronizer): AQS 可以理解為就是 一個(gè)可以實(shí)現(xiàn)鎖的框架

簡(jiǎn)單的流程理解:

公平鎖:

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?
  • 第一步:獲取狀態(tài)的 state 的值。如果 state=0 即代表鎖沒(méi)有被其它線程占用,執(zhí)行第二步。如果 state!=0 則代表鎖正在被其它線程占用,執(zhí)行第三步。
  • 第二步:判斷隊(duì)列中是否有線程在排隊(duì)等待。如果不存在則直接將鎖的所有者設(shè)置成當(dāng)前線程,且更新?tīng)顟B(tài) state 。如果存在就入隊(duì)。
  • 第三步:判斷鎖的所有者是不是當(dāng)前線程。如果是則更新?tīng)顟B(tài) state 的值。如果不是,線程進(jìn)入隊(duì)列排隊(duì)等待。

非公平鎖:

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?
  • 第一步:獲取狀態(tài)的 state 的值。如果 state=0 即代表鎖沒(méi)有被其它線程占用,則設(shè)置當(dāng)前鎖的持有者為當(dāng)前線程,該操作用 CAS 完成。如果不為0或者設(shè)置失敗,代表鎖被占用進(jìn)行下一步。
  • 此時(shí)獲取 state 的值,如果是0,代表剛好線程釋放了鎖,此時(shí)將鎖的持有者設(shè)為自己如果不是0,則查看線程持有者是不是自己如果是,則給state+1,獲取鎖如果不是,則進(jìn)入隊(duì)列等待

讀完以上的部分相信你對(duì)AQS已經(jīng)有了一個(gè)比較清楚的概念了,所以我們來(lái)聊聊小細(xì)節(jié)。

AQS使用state同步狀態(tài)(0代表無(wú)鎖,1代表有),并暴露出 getState 、 setState 以及 compareAndSet 操作來(lái)讀取和更新這個(gè)狀態(tài),使得僅當(dāng)同步狀態(tài)擁有一個(gè)期望值的時(shí)候,才會(huì)被原子地設(shè)置成新值。

當(dāng)有線程獲取鎖失敗后,AQS是通過(guò)一個(gè) 雙向的同步隊(duì)列 來(lái)完成同步狀態(tài)的管理,就被添加到隊(duì)列末尾。

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

這是定義頭尾節(jié)點(diǎn)的代碼,我們可以先使用 volatile 去修飾的,就是保證讓其他線程可見(jiàn), AQS 實(shí)際上就是修改頭尾兩個(gè)節(jié)點(diǎn)來(lái)完成入隊(duì)和出隊(duì)操作的。

AQS 在鎖的獲取時(shí),并不一定只有一個(gè)線程才能持有這個(gè)鎖,所以此時(shí)有了 獨(dú)占模式和共享模式 的區(qū)別,我們本篇文章中的 ReentrantLock 使用的就是獨(dú)占模式,在多線程的情況下只會(huì)有一個(gè)線程獲取鎖。

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

獨(dú)占模式的流程是比較簡(jiǎn)單的,就根據(jù)state是否為0來(lái)判斷是否有線程已經(jīng)獲得了鎖,沒(méi)有就阻塞,有就繼續(xù)執(zhí)行后續(xù)代碼邏輯。

動(dòng)態(tài)高并發(fā)時(shí)為什么推薦重入鎖而不是Synchronized?

共享模式的流程根據(jù)state是否大于0來(lái)判斷是否有線程已經(jīng)獲得了鎖,如果不大于0,就阻塞,如果大于0,通過(guò)CAS的原子操作來(lái)自減state的值,然后繼續(xù)執(zhí)行后續(xù)代碼邏輯。

ReentrantLock和Synchronized的區(qū)別

  • 其實(shí)ReentrantLock和Synchronized 最核心的區(qū)別就在于 Synchronized適合于并發(fā)競(jìng)爭(zhēng)低的情況,因?yàn)镾ynchronized的鎖升級(jí)如果最終升級(jí)為重量級(jí)鎖在使用的過(guò)程中是沒(méi)有辦法消除的,意味著每次都要和cpu去請(qǐng)求鎖資源,而ReentrantLock主要是提供了阻塞的能力, 通過(guò)在高并發(fā)下線程的掛起,來(lái)減少競(jìng)爭(zhēng),提高并發(fā)能力 ,所以我們文章標(biāo)題的答案,也就顯而易見(jiàn)了。
  • synchronized是一個(gè)關(guān)鍵字,是由 jvm層面 去實(shí)現(xiàn)的,而ReentrantLock是由 java api 去實(shí)現(xiàn)的。
  • synchronized是隱式鎖,可以 自動(dòng)釋放鎖 ,ReentrantLock是顯式鎖,需要 手動(dòng)釋放鎖 。
  • ReentrantLock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行,使用synchronized時(shí),等待的線程會(huì)一直等待下去, 不能夠響應(yīng)中斷。
  • ReentrantLock可以獲取鎖狀態(tài),而synchronized不能。

說(shuō)說(shuō)標(biāo)題的答案

其實(shí)題目的答案就在上一欄目的第一條,也是核心的區(qū)別,synchronized升級(jí)為重量級(jí)鎖后無(wú)法在正常情況下完成降級(jí),而ReentrantLock是通過(guò)阻塞來(lái)提高性能的,在設(shè)計(jì)模式上就體現(xiàn)出了對(duì)多線程情況的支持。

來(lái)源:https://www.tuicool.com/articles/A36jUbf

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容