- Java并發(fā)編程深度解析與實(shí)戰(zhàn)
- 譚鋒(Mic)
- 2971字
- 2022-05-10 18:39:17
2.5 synchronized的鎖類型
根據(jù)前面我們對(duì)同步鎖的理解,實(shí)現(xiàn)同步鎖的方式無(wú)非是多個(gè)線程搶占一個(gè)互斥變量,如果搶占成功則表示獲得了鎖,而沒(méi)有獲得鎖的線程則阻塞等待,直到獲得鎖的線程釋放鎖。
如圖2-12所示,在Mark Word中,我們發(fā)現(xiàn)鎖的類型有偏向鎖、輕量級(jí)鎖、重量級(jí)鎖,那么這些鎖有什么含義呢?

圖2-12 64位虛擬機(jī)的Mark Word
其實(shí),在JDK 1.6之前,synchronized只提供了重量級(jí)鎖的機(jī)制,重量級(jí)鎖的本質(zhì)就是我們前面對(duì)于鎖的認(rèn)知,也就是沒(méi)有獲得鎖的線程會(huì)通過(guò)park方法阻塞,接著被獲得鎖的線程喚醒后再次搶占鎖,直到搶占成功。
重量級(jí)鎖依賴于底層操作系統(tǒng)的Mutex Lock來(lái)實(shí)現(xiàn),而使用Mutex Lock需要把當(dāng)前線程掛起,并從用戶態(tài)切換到內(nèi)核態(tài)來(lái)執(zhí)行,這種切換帶來(lái)的性能開(kāi)銷是非常大的。因此,如何在性能和線程安全性之間做好平衡,就是一個(gè)值得探討的話題了。
在JDK 1.6之后,synchronized做了很多優(yōu)化,其中針對(duì)鎖的類型增加了偏向鎖和輕量級(jí)鎖,這兩種鎖的核心設(shè)計(jì)理念就是如何讓線程在不阻塞的情況下達(dá)到線程安全的目的。
2.5.1 偏向鎖的原理分析
偏向鎖其實(shí)可以認(rèn)為是在沒(méi)有多線程競(jìng)爭(zhēng)的情況下訪問(wèn)synchronized修飾的代碼塊的加鎖場(chǎng)景,也就是在單線程執(zhí)行的情況下。
很多讀者可能會(huì)有疑問(wèn),沒(méi)有線程競(jìng)爭(zhēng),那為什么要加鎖呢?實(shí)際上對(duì)程序開(kāi)發(fā)來(lái)說(shuō),加鎖是為了防范線程安全性的風(fēng)險(xiǎn),但是是否有線程競(jìng)爭(zhēng)并不由我們來(lái)控制,而是由應(yīng)用場(chǎng)景來(lái)決定。假設(shè)這種情況存在,就沒(méi)有必要使用重量級(jí)鎖基于操作系統(tǒng)級(jí)別的Mutex Lock來(lái)實(shí)現(xiàn)鎖的搶占,這樣顯然很耗費(fèi)性能。
所以偏向鎖的作用就是,線程在沒(méi)有線程競(jìng)爭(zhēng)的情況下去訪問(wèn)synchronized同步代碼塊時(shí),會(huì)嘗試先通過(guò)偏向鎖來(lái)?yè)屨荚L問(wèn)資格,這個(gè)搶占過(guò)程是基于CAS來(lái)完成的,如果搶占鎖成功,則直接修改對(duì)象頭中的鎖標(biāo)記。其中,偏向鎖標(biāo)記為1,鎖標(biāo)記為01,以及存儲(chǔ)當(dāng)前獲得鎖的線程ID。而偏向的意思就是,如果線程X獲得了偏向鎖,那么當(dāng)線程X后續(xù)再訪問(wèn)這個(gè)同步方法時(shí),只需要判斷對(duì)象頭中的線程ID和線程X是否相等即可。如果相等,就不需要再次去搶占鎖,直接獲得訪問(wèn)資格即可,其實(shí)現(xiàn)原理如圖2-13所示。

圖2-13 偏向鎖的獲得鎖邏輯
結(jié)合前面關(guān)于對(duì)象頭部分的說(shuō)明及偏向鎖的原理,我們通過(guò)一個(gè)例子來(lái)看一下偏向鎖的實(shí)現(xiàn)。

在上述代碼中,BiasedLockExample演示了針對(duì)example這個(gè)鎖對(duì)象,在加鎖之前和加鎖之后分別打印對(duì)象的內(nèi)存布局的過(guò)程,來(lái)看一下輸出結(jié)果。


從上述輸出結(jié)果中我們發(fā)現(xiàn):
? 在加鎖之前,對(duì)象頭中的第一個(gè)字節(jié)00000001最后三位為[001],其中低位的兩位表示鎖標(biāo)記,它的值是[01],表示當(dāng)前為無(wú)鎖狀態(tài)。
? 在加鎖之后,對(duì)象頭中的第一個(gè)字節(jié)01111000最后三位為[000],其中低位的兩位是[00],對(duì)照前面介紹的Mark Word中的存儲(chǔ)結(jié)構(gòu)的含義,它表示輕量級(jí)鎖狀態(tài)。
當(dāng)前的程序并不存在鎖競(jìng)爭(zhēng),基于前面的理論分析,此處應(yīng)該是獲得偏向鎖,但是為什么變成了輕量級(jí)鎖呢?
原因是,JVM在啟動(dòng)的時(shí)候,有一個(gè)啟動(dòng)參數(shù)-XX:BiasedLockingStartupDelay,這個(gè)參數(shù)表示偏向鎖延遲開(kāi)啟的時(shí)間,默認(rèn)是4秒,也就是說(shuō)在我們運(yùn)行上述程序時(shí),偏向鎖還未開(kāi)啟,導(dǎo)致最終只能獲得輕量級(jí)鎖。之所以延遲啟動(dòng),是因?yàn)镴VM在啟動(dòng)的時(shí)候會(huì)有很多線程運(yùn)行,也就是說(shuō)會(huì)存在線程競(jìng)爭(zhēng)的場(chǎng)景,那么這時(shí)候開(kāi)啟偏向鎖的意義不大。
如果我們需要看到偏向鎖的實(shí)現(xiàn)效果,那么有兩種方法:
? 添加JVM啟動(dòng)參數(shù)-XX:BiasedLockingStartupDelay=0,把延遲啟動(dòng)時(shí)間設(shè)置為0。
? 搶占鎖資源之前,先通過(guò)Thread.sleep()方法睡眠4秒以上。
最終得到如下輸出結(jié)果。

從上面輸出結(jié)果我們發(fā)現(xiàn),加鎖之后,第一個(gè)字節(jié)低位部分的3位變成了[101],高位[1]表示當(dāng)前是偏向鎖狀態(tài),低位[01]表示當(dāng)前是偏向鎖狀態(tài),這顯然達(dá)到了我們的預(yù)期效果。細(xì)心的讀者會(huì)發(fā)現(xiàn),加鎖之前的鎖標(biāo)記位也是[101]——這里并沒(méi)有加偏向鎖呀?
我們來(lái)分析一下,加鎖之前并沒(méi)有存儲(chǔ)線程ID,加鎖之后才有一個(gè)線程ID(45889541)。因此,在獲得偏向鎖之前,這個(gè)標(biāo)記表示當(dāng)前是可偏向狀態(tài),并不代表已經(jīng)處于偏向狀態(tài)。

2.5.2 輕量級(jí)鎖的原理分析
在線程沒(méi)有競(jìng)爭(zhēng)時(shí),使用偏向鎖能夠在不影響性能的前提下獲得鎖資源,但是同一時(shí)刻只允許一個(gè)線程獲得鎖資源,如果突然有多個(gè)線程來(lái)訪問(wèn)同步方法,那么沒(méi)有搶占到鎖資源的線程要怎么辦呢?很顯然偏向鎖解決不了這個(gè)問(wèn)題。
正常情況下,沒(méi)有搶占到鎖的線程肯定要阻塞等待被喚醒,也就是說(shuō)按照重量級(jí)鎖的邏輯來(lái)實(shí)現(xiàn),但是在此之前,有沒(méi)有更好的平衡方案呢?于是就有了輕量級(jí)鎖的設(shè)計(jì)。
所謂的輕量級(jí)鎖,就是沒(méi)有搶占到鎖的線程,進(jìn)行一定次數(shù)的重試(自旋)。比如線程第一次沒(méi)搶到鎖則重試幾次,如果在重試的過(guò)程中搶占到了鎖,那么這個(gè)線程就不需要阻塞,這種實(shí)現(xiàn)方式我們稱為自旋鎖,具體的實(shí)現(xiàn)流程如圖2-14所示。

圖2-14 輕量級(jí)鎖實(shí)現(xiàn)流程圖
當(dāng)然,線程通過(guò)重試來(lái)?yè)屨兼i的方式是有代價(jià)的,因?yàn)榫€程如果不斷自旋重試,那么CPU會(huì)一直處于運(yùn)行狀態(tài)。如果持有鎖的線程占有鎖的時(shí)間比較短,那么自旋等待的實(shí)現(xiàn)帶來(lái)性能的提升會(huì)比較明顯。反之,如果持有鎖的線程占用鎖資源的時(shí)間比較長(zhǎng),那么自旋的線程就會(huì)浪費(fèi)CPU資源,所以線程重試搶占鎖的次數(shù)必須要有一個(gè)限制。
在JDK 1.6中默認(rèn)的自旋次數(shù)是10次,我們可以通過(guò)-XX:PreBlockSpin參數(shù)來(lái)調(diào)整自旋次數(shù)。同時(shí)開(kāi)發(fā)者在JDK 1.6中還對(duì)自旋鎖做了優(yōu)化,引入了自適應(yīng)自旋鎖,自適應(yīng)自旋鎖的自旋次數(shù)不是固定的,而是根據(jù)前一次在同一個(gè)鎖上的自旋次數(shù)及鎖持有者的狀態(tài)來(lái)決定的。如果在同一個(gè)鎖對(duì)象上,通過(guò)自旋等待成功獲得過(guò)鎖,并且持有鎖的線程正在運(yùn)行中,那么JVM會(huì)認(rèn)為此次自旋也有很大的機(jī)會(huì)獲得鎖,因此會(huì)將這個(gè)線程的自旋時(shí)間相對(duì)延長(zhǎng)。反之,如果在一個(gè)鎖對(duì)象中,通過(guò)自旋鎖獲得鎖很少成功,那么JVM會(huì)縮短自旋次數(shù)。
輕量級(jí)鎖的演示在2.5.1節(jié)中有,默認(rèn)不修改偏向鎖的延期開(kāi)啟參數(shù),加鎖得到的鎖狀態(tài)就是輕量級(jí)鎖。

2.5.3 重量級(jí)鎖的原理分析
輕量級(jí)鎖能夠通過(guò)一定次數(shù)的重試讓沒(méi)有獲得鎖的線程有可能搶占到鎖資源,但是輕量級(jí)鎖只有在獲得鎖的線程持有鎖的時(shí)間較短的情況下才能起到提升同步鎖性能的效果。如果持有鎖的線程占用鎖資源的時(shí)間較長(zhǎng),那么不能讓那些沒(méi)有搶占到鎖資源的線程不斷自旋,否則會(huì)占用過(guò)多的CPU資源,這反而是一件得不償失的事情。
如果沒(méi)搶占到鎖資源的線程通過(guò)一定次數(shù)的自旋后,發(fā)現(xiàn)仍然沒(méi)有獲得鎖,就只能阻塞等待了,所以最終會(huì)升級(jí)到重量級(jí)鎖,通過(guò)系統(tǒng)層面的互斥量來(lái)?yè)屨兼i資源。重量級(jí)鎖的實(shí)現(xiàn)原理如圖2-15所示。

圖2-15 重量級(jí)鎖的實(shí)現(xiàn)原理
整體來(lái)看,我們發(fā)現(xiàn),如果在偏向鎖、輕量級(jí)鎖這些類型中無(wú)法讓線程獲得鎖資源,那么這些沒(méi)獲得鎖的線程最終的結(jié)果仍然是阻塞等待,直到獲得鎖的線程釋放鎖之后才能被喚醒。而在整個(gè)優(yōu)化過(guò)程中,我們通過(guò)樂(lè)觀鎖的機(jī)制來(lái)保證線程的安全性。
下面這個(gè)例子演示了在加鎖之前、單個(gè)線程搶占鎖、多個(gè)線程搶占鎖的場(chǎng)景中,對(duì)象頭中的鎖的狀態(tài)變化。


上述程序打印的結(jié)果如下。


從上述打印結(jié)果來(lái)看,對(duì)象頭中的鎖狀態(tài)一共經(jīng)歷了三個(gè)類型。
? 加鎖之前,對(duì)象頭中的第一個(gè)字節(jié)是00000001,表示無(wú)鎖狀態(tài)。
? 當(dāng)t1線程去搶占同步鎖時(shí),對(duì)象頭中的第一個(gè)字節(jié)變成了11011000,表示輕量級(jí)鎖狀態(tài)。
? 接著main線程來(lái)?yè)屨纪粋€(gè)對(duì)象鎖,由于t1線程睡眠了2秒,此時(shí)鎖還沒(méi)有被釋放,main線程無(wú)法通過(guò)輕量級(jí)鎖自旋獲得鎖,因此它的鎖的類型是重量級(jí)鎖,鎖標(biāo)記為10。
注意,在這個(gè)案例演示中,筆者并沒(méi)有開(kāi)啟偏向鎖的參數(shù),如果開(kāi)啟了,那么第一個(gè)加鎖之后得到的鎖狀態(tài)應(yīng)該是偏向鎖,然后直接到重量級(jí)鎖(因?yàn)閠1線程有一個(gè)sleep,所以輕量級(jí)鎖肯定無(wú)法獲得)。
由此可以看到,synchronized同步鎖最終的底層加鎖機(jī)制是JVM層面根據(jù)線程的競(jìng)爭(zhēng)情況逐步升級(jí)來(lái)實(shí)現(xiàn)的,從而達(dá)到同步鎖性能和安全性平衡的目的,而這個(gè)過(guò)程并不需要開(kāi)發(fā)者干預(yù)。
- Learning Single:page Web Application Development
- 騰訊iOS測(cè)試實(shí)踐
- ASP.NET Core Essentials
- Hands-On Data Structures and Algorithms with JavaScript
- Scratch 3.0少兒編程與邏輯思維訓(xùn)練
- Flux Architecture
- JavaScript 程序設(shè)計(jì)案例教程
- Learning Hunk
- C++寶典
- 平面設(shè)計(jì)經(jīng)典案例教程:CorelDRAW X6
- 自學(xué)Python:編程基礎(chǔ)、科學(xué)計(jì)算及數(shù)據(jù)分析(第2版)
- Deep Learning with R Cookbook
- 嵌入式Linux C語(yǔ)言程序設(shè)計(jì)基礎(chǔ)教程
- 微前端設(shè)計(jì)與實(shí)現(xiàn)
- Python數(shù)據(jù)科學(xué)實(shí)踐指南