synchronized實(shí)現(xiàn)原理
1. 實(shí)現(xiàn)原理
synchronized
可以保證方法或者代碼塊在運(yùn)行時(shí)审编,同一時(shí)刻只有一個(gè)方法可以進(jìn)入到臨界區(qū)质礼,同時(shí)它還可以保證共享變量的內(nèi)存可見(jiàn)性试浙。
Java 中每一個(gè)對(duì)象都可以作為鎖,這是 synchronized
實(shí)現(xiàn)同步的基礎(chǔ):
普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象
靜態(tài)同步方法识颊,鎖是當(dāng)前類(lèi)的 class 對(duì)象
同步方法塊耿焊,鎖是括號(hào)里面的對(duì)象
當(dāng)一個(gè)線(xiàn)程訪(fǎng)問(wèn)同步代碼塊時(shí)揪惦,它首先是需要得到鎖才能執(zhí)行同步代碼,當(dāng)退出或者拋出異常時(shí)必須要釋放鎖罗侯,那么它是如何來(lái)實(shí)現(xiàn)這個(gè)機(jī)制的呢器腋?我們先看一段簡(jiǎn)單的代碼:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n16" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">public class SynchronizedTest {
public synchronized void test1() {
}
public void test2() {
synchronized(this) {
}
}
}</pre>
利用 Javap 工具查看生成的 class 文件信息來(lái)分析 synchronized
的實(shí)現(xiàn)
從上面可以看出:
1)同步代碼塊是使用 monitorenter
和 monitorexit
指令實(shí)現(xiàn)的;
同步代碼塊:monitorenter
指令插入到同步代碼塊的開(kāi)始位置钩杰,monitorexit
指令插入到同步代碼塊的結(jié)束位置纫塌,JVM 需要保證每一個(gè) monitorenter
都有一個(gè) monitorexit
與之相對(duì)應(yīng)。任何對(duì)象都有一個(gè) Monitor 與之相關(guān)聯(lián)讲弄,當(dāng)且一個(gè) Monitor 被持有之后措左,他將處于鎖定狀態(tài)。線(xiàn)程執(zhí)行到 monitorenter
指令時(shí)避除,將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的 Monitor 所有權(quán)怎披,即嘗試獲取對(duì)象的鎖胸嘁。
2)同步方法(在這看不出來(lái)需要看JVM底層實(shí)現(xiàn))依靠的是方法修飾符上的ACC_SYNCHRONIZED
實(shí)現(xiàn)。
同步代碼塊:
monitorenter
指令插入到同步代碼塊的開(kāi)始位置钳枕,monitorexit
指令插入到同步代碼塊的結(jié)束位置缴渊,JVM 需要保證每一個(gè)monitorenter
都有一個(gè)monitorexit
與之相對(duì)應(yīng)。任何對(duì)象都有一個(gè) Monitor 與之相關(guān)聯(lián)鱼炒,當(dāng)且一個(gè) Monitor 被持有之后衔沼,他將處于鎖定狀態(tài)。線(xiàn)程執(zhí)行到monitorenter
指令時(shí)昔瞧,將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的 Monitor 所有權(quán)指蚁,即嘗試獲取對(duì)象的鎖。-
accessFlags
是類(lèi)和方法的訪(fǎng)問(wèn)標(biāo)志自晰,總共 16 bits 凝化。具體每 1 bit 的表示,可參見(jiàn) AccessFlag.java 酬荞。
- 關(guān)于 Klass 搓劫,感興趣的胖友,可以看看 《【理解HotSpot虛擬機(jī)】對(duì)象在 JVM 中的表示:OOP-Klass 模型》 混巧。
- 關(guān)于同步代碼塊和同步方法的更加詳細(xì)的實(shí)現(xiàn)原理枪向,可參見(jiàn) 《Java 8 并發(fā)篇 - 冷靜分析 Synchronized(下)》 「4.3 同步代碼塊同步原理」 部分,寫(xiě)的非常棒咧党。
-
2. Java 對(duì)象頭秘蛔、Monitor
Java 對(duì)象頭和 Monitor 是實(shí)現(xiàn) synchronized
的基礎(chǔ)!下面就這兩個(gè)概念來(lái)做詳細(xì)介紹傍衡。
2.1 Java對(duì)象頭
synchronized
用的鎖是存在Java對(duì)象頭里的深员。那么什么是 Java 對(duì)象頭呢?Hotspot 虛擬機(jī)的對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)蛙埂、Klass Pointer(類(lèi)型指針)倦畅。其中:
Klass Point 是是對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例绣的。
關(guān)于 Klass Point 和 Mark Word 的說(shuō)明叠赐,感興趣的胖友,可以看看 《【理解HotSpot虛擬機(jī)】對(duì)象在 JVM 中的表示:OOP-Klass 模型》 的 「實(shí)例說(shuō)明」被辑。
Mark Word 用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)敬惦、GC 分代年齡盼理、鎖狀態(tài)標(biāo)志、線(xiàn)程持有的鎖俄删、偏向線(xiàn)程 ID宏怔、偏向時(shí)間戳等等奏路。Java 對(duì)象頭一般占有兩個(gè)機(jī)器碼(在 32 位虛擬機(jī)中,1 個(gè)機(jī)器碼等于 4 字節(jié)臊诊,也就是 32 bits)鸽粉。但是如果對(duì)象是數(shù)組類(lèi)型,則需要三個(gè)機(jī)器碼抓艳,因?yàn)?JVM 虛擬機(jī)可以通過(guò) Java 對(duì)象的元數(shù)據(jù)信息確定 Java 對(duì)象的大小触机,無(wú)法從數(shù)組的元數(shù)據(jù)來(lái)確認(rèn)數(shù)組的大小,所以用一塊來(lái)記錄數(shù)組長(zhǎng)度玷或。
下圖是 Java 對(duì)象頭的存儲(chǔ)結(jié)構(gòu)(32位虛擬機(jī)):
對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本儡首,但是考慮到虛擬機(jī)的空間效率,Mark Word 被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)偏友,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間蔬胯,也就是說(shuō),Mark Word 會(huì)隨著程序的運(yùn)行發(fā)生變化位他,變化狀態(tài)如下:
-
32 位虛擬機(jī):
- 每一行氛濒,是一種情況。
-
64 位虛擬機(jī):
- 對(duì)于 32 位無(wú)鎖狀態(tài)鹅髓,有 25 bits 沒(méi)有使用舞竿。
簡(jiǎn)單介紹了 Java 對(duì)象頭,我們下面再看 Monitor迈勋。
2.2 Monitor
FROM 《Java 8 并發(fā)篇 - 冷靜分析 Synchronized(下)》
- 互斥: 一個(gè) Monitor 鎖在同一時(shí)刻只能被一個(gè)線(xiàn)程占用炬灭,其他線(xiàn)程無(wú)法占用。
- 信號(hào)機(jī)制( signal ): 占用 Monitor 鎖失敗的線(xiàn)程會(huì)暫時(shí)放棄競(jìng)爭(zhēng)并等待某個(gè)謂詞成真(條件變量)靡菇,但該條件成立后重归,當(dāng)前線(xiàn)程會(huì)通過(guò)釋放鎖通知正在等待這個(gè)條件變量的其他線(xiàn)程,讓其可以重新競(jìng)爭(zhēng)鎖厦凤。
FROM 《Java并發(fā)編程的藝術(shù)》的 「2.2 synchronized 的實(shí)現(xiàn)原理與引用」 章節(jié)鼻吮。
Monitor Record 是線(xiàn)程私有的數(shù)據(jù)結(jié)構(gòu),每一個(gè)線(xiàn)程都有一個(gè)可用 Monitor Record 列表较鼓,同時(shí)還有一個(gè)全局的可用列表椎木。 每一個(gè)被鎖住的對(duì)象都會(huì)和一個(gè) Monitor Record 關(guān)聯(lián)(對(duì)象頭的 MarkWord 中的 LockWord 指向 Monitor 的起始地址),Monitor Record 中有一個(gè) Owner 字段博烂,存放擁有該鎖的線(xiàn)程的唯一標(biāo)識(shí)香椎,表示該鎖被這個(gè)線(xiàn)程占用。其結(jié)構(gòu)如下:
- Owner:1)初始時(shí)為 NULL 表示當(dāng)前沒(méi)有任何線(xiàn)程擁有該 Monitor Record禽篱;2)當(dāng)線(xiàn)程成功擁有該鎖后保存線(xiàn)程唯一標(biāo)識(shí)畜伐;3)當(dāng)鎖被釋放時(shí)又設(shè)置為 NULL 。
- EntryQ:關(guān)聯(lián)一個(gè)系統(tǒng)互斥鎖( semaphore )躺率,阻塞所有試圖鎖住 Monitor Record失敗的線(xiàn)程 玛界。
- RcThis:表示 blocked 或 waiting 在該 Monitor Record 上的所有線(xiàn)程的個(gè)數(shù)万矾。
- Nest:用來(lái)實(shí)現(xiàn)重入鎖的計(jì)數(shù)。
- HashCode:保存從對(duì)象頭拷貝過(guò)來(lái)的 HashCode 值(可能還包含 GC age )慎框。
- Candidate:用來(lái)避免不必要的阻塞或等待線(xiàn)程喚醒良狈。因?yàn)槊恳淮沃挥幸粋€(gè)線(xiàn)程能夠成功擁有鎖,如果每次前一個(gè)釋放鎖的線(xiàn)程喚醒所有正在阻塞或等待的線(xiàn)程笨枯,會(huì)引起不必要的上下文切換(從阻塞到就緒然后因?yàn)楦?jìng)爭(zhēng)鎖失敗又被阻塞)從而導(dǎo)致性能?chē)?yán)重下降薪丁。Candidate 只有兩種可能的值 :1)0 表示沒(méi)有需要喚醒的線(xiàn)程;2)1 表示要喚醒一個(gè)繼任線(xiàn)程來(lái)競(jìng)爭(zhēng)鎖猎醇。
我們知道 synchronized
是重量級(jí)鎖窥突,效率不怎么滴,同時(shí)這個(gè)觀念也一直存在我們腦海里硫嘶,不過(guò)在 JDK 1.6 中對(duì) synchronize
的實(shí)現(xiàn)進(jìn)行了各種優(yōu)化阻问,使得它顯得不是那么重了,那么 JVM 采用了那些優(yōu)化手段呢沦疾?
3. 鎖優(yōu)化
簡(jiǎn)單來(lái)說(shuō)称近,在 JVM 中
monitorenter
和monitorexit
字節(jié)碼依賴(lài)于底層的操作系統(tǒng)的Mutex Lock 來(lái)實(shí)現(xiàn)的,但是由于使用 Mutex Lock 需要將當(dāng)前線(xiàn)程掛起并從用戶(hù)態(tài)切換到內(nèi)核態(tài)來(lái)執(zhí)行哮塞,這種切換的代價(jià)是非常昂貴的刨秆。然而,在現(xiàn)實(shí)中的大部分情況下忆畅,同步方法是運(yùn)行在單線(xiàn)程環(huán)境(無(wú)鎖競(jìng)爭(zhēng)環(huán)境)衡未,如果每次都調(diào)用 Mutex Lock 那么將嚴(yán)重的影響程序的性能。
因此家凯,JDK 1.6 對(duì)鎖的實(shí)現(xiàn)引入了大量的優(yōu)化缓醋,如自旋鎖、適應(yīng)性自旋鎖绊诲、鎖消除送粱、鎖粗化、偏向鎖掂之、輕量級(jí)鎖等技術(shù)來(lái)減少鎖操作的開(kāi)銷(xiāo)抗俄。
3.1 自旋鎖
由來(lái)
線(xiàn)程的阻塞和喚醒,需要 CPU 從用戶(hù)態(tài)轉(zhuǎn)為核心態(tài)世舰。頻繁的阻塞和喚醒對(duì) CPU 來(lái)說(shuō)是一件負(fù)擔(dān)很重的工作动雹,勢(shì)必會(huì)給系統(tǒng)的并發(fā)性能帶來(lái)很大的壓力。同時(shí)跟压,我們發(fā)現(xiàn)在許多應(yīng)用上面胰蝠,對(duì)象鎖的鎖狀態(tài)只會(huì)持續(xù)很短一段時(shí)間。為了這一段很短的時(shí)間,頻繁地阻塞和喚醒線(xiàn)程是非常不值得的姊氓。所以引入自旋鎖。
定義
所謂自旋鎖喷好,就是讓該線(xiàn)程等待一段時(shí)間翔横,不會(huì)被立即掛起,看持有鎖的線(xiàn)程是否會(huì)很快釋放鎖梗搅。
怎么等待呢禾唁?執(zhí)行一段無(wú)意義的循環(huán)即可(自旋)。
自旋等待不能替代阻塞无切,先不說(shuō)對(duì)處理器數(shù)量的要求(多核荡短,貌似現(xiàn)在沒(méi)有單核的處理器了),雖然它可以避免線(xiàn)程切換帶來(lái)的開(kāi)銷(xiāo)哆键,但是它占用了處理器的時(shí)間掘托。如果持有鎖的線(xiàn)程很快就釋放了鎖,那么自旋的效率就非常好籍嘹,反之闪盔,自旋的線(xiàn)程就會(huì)白白消耗掉處理的資源,它不會(huì)做任何有意義的工作辱士,典型的占著茅坑不拉屎泪掀,這樣反而會(huì)帶來(lái)性能上的浪費(fèi)。
所以說(shuō)颂碘,自旋等待的時(shí)間(自旋的次數(shù))必須要有一個(gè)限度异赫,如果自旋超過(guò)了定義的時(shí)間仍然沒(méi)有獲取到鎖,則應(yīng)該被掛起头岔。
自旋鎖在 JDK 1.4.2 中引入塔拳,默認(rèn)關(guān)閉,但是可以使用 -XX:+UseSpinning
開(kāi)開(kāi)啟切油。 在 JDK1.6 中默認(rèn)開(kāi)啟蝙斜。同時(shí)自旋的默認(rèn)次數(shù)為 10 次,可以通過(guò)參數(shù) -XX:PreBlockSpin
來(lái)調(diào)整澎胡。
如果通過(guò)參數(shù) -XX:PreBlockSpin
來(lái)調(diào)整自旋鎖的自旋次數(shù)孕荠,會(huì)帶來(lái)諸多不便。假如我將參數(shù)調(diào)整為 10 攻谁,但是系統(tǒng)很多線(xiàn)程都是等你剛剛退出的時(shí)候稚伍,就釋放了鎖(假如你多自旋一兩次就可以獲取鎖),你是不是很尷尬戚宦。于是 JDK 1.6 引入自適應(yīng)的自旋鎖个曙,讓虛擬機(jī)會(huì)變得越來(lái)越聰明。
3.1.1 適應(yīng)自旋鎖
JDK 1.6 引入了更加聰明的自旋鎖,即自適應(yīng)自旋鎖垦搬。
所謂自適應(yīng)就意味著自旋的次數(shù)不再是固定的呼寸,它是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定。它怎么做呢猴贰?
線(xiàn)程如果自旋成功了对雪,那么下次自旋的次數(shù)會(huì)更加多,因?yàn)樘摂M機(jī)認(rèn)為既然上次成功了米绕,那么此次自旋也很有可能會(huì)再次成功瑟捣,那么它就會(huì)允許自旋等待持續(xù)的次數(shù)更多。
反之栅干,如果對(duì)于某個(gè)鎖迈套,很少有自旋能夠成功的,那么在以后要或者這個(gè)鎖的時(shí)候自旋的次數(shù)會(huì)減少甚至省略掉自旋過(guò)程碱鳞,以免浪費(fèi)處理器資源桑李。
有了自適應(yīng)自旋鎖,隨著程序運(yùn)行和性能監(jiān)控信息的不斷完善窿给,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)會(huì)越來(lái)越準(zhǔn)確芙扎,虛擬機(jī)會(huì)變得越來(lái)越聰明。
3.2 鎖消除
由來(lái)
為了保證數(shù)據(jù)的完整性填大,我們?cè)谶M(jìn)行操作時(shí)需要對(duì)這部分操作進(jìn)行同步控制戒洼。但是,在有些情況下允华,JVM檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)圈浇,這是JVM會(huì)對(duì)這些同步鎖進(jìn)行鎖消除。如果不存在競(jìng)爭(zhēng)靴寂,為什么還需要加鎖呢磷蜀?所以鎖消除可以節(jié)省毫無(wú)意義的請(qǐng)求鎖的時(shí)間。
定義
鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持百炬。變量是否逃逸褐隆,對(duì)于虛擬機(jī)來(lái)說(shuō)需要使用數(shù)據(jù)流分析來(lái)確定,但是對(duì)于我們程序員來(lái)說(shuō)這還不清楚么剖踊?我們會(huì)在明明知道不存在數(shù)據(jù)競(jìng)爭(zhēng)的代碼塊前加上同步嗎庶弃?但是有時(shí)候程序并不是我們所想的那樣?我們雖然沒(méi)有顯示使用鎖德澈,但是我們?cè)谑褂靡恍?JDK 的內(nèi)置 API 時(shí)歇攻,如 StringBuffer、Vector梆造、HashTable 等缴守,這個(gè)時(shí)候會(huì)存在隱性的加鎖操作。比如 StringBuffer 的 #append(..)
方法,Vector 的 add(...)
方法:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n122" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">public void vectorTest(){
Vector<String> vector = new Vector<String>();
for (int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}</pre>
在運(yùn)行這段代碼時(shí)屡穗,JVM 可以明顯檢測(cè)到變量 vector
沒(méi)有逃逸出方法 #vectorTest()
之外贴捡,所以 JVM 可以大膽地將 vector
內(nèi)部的加鎖操作消除。
3.3 鎖粗化
由來(lái)
我們知道在使用同步鎖的時(shí)候村砂,需要讓同步塊的作用范圍盡可能姓幌尽:僅在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步。這樣做的目的箍镜,是為了使需要同步的操作數(shù)量盡可能縮小,如果存在鎖競(jìng)爭(zhēng)煎源,那么等待鎖的線(xiàn)程也能盡快拿到鎖色迂。
在大多數(shù)的情況下,上述觀點(diǎn)是正確的手销,LZ 也一直堅(jiān)持著這個(gè)觀點(diǎn)歇僧。但是如果一系列的連續(xù)加鎖解鎖操作,可能會(huì)導(dǎo)致不必要的性能損耗锋拖,所以引入鎖粗話(huà)的概念诈悍。
定義
鎖粗話(huà)概念比較好理解,就是將多個(gè)連續(xù)的加鎖兽埃、解鎖操作連接在一起侥钳,擴(kuò)展成一個(gè)范圍更大的鎖。
如上面實(shí)例:vector
每次 add 的時(shí)候都需要加鎖操作柄错,JVM 檢測(cè)到對(duì)同一個(gè)對(duì)象(vector
)連續(xù)加鎖舷夺、解鎖操作,會(huì)合并一個(gè)更大范圍的加鎖售貌、解鎖操作给猾,即加鎖解鎖操作會(huì)移到 for
循環(huán)之外。
3.4 鎖的升級(jí)
鎖主要存在四種狀態(tài)颂跨,依次是:無(wú)鎖狀態(tài)敢伸、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)恒削、重量級(jí)鎖狀態(tài)池颈。它們會(huì)隨著競(jìng)爭(zhēng)的激烈而逐漸升級(jí)。注意钓丰,鎖可以升級(jí)不可降級(jí)饶辙,這種策略是為了提高獲得鎖和釋放鎖的效率。
3.4.1 重量級(jí)鎖
重量級(jí)鎖通過(guò)對(duì)象內(nèi)部的監(jiān)視器(Monitor)實(shí)現(xiàn)斑粱。
其中弃揽,Monitor 的本質(zhì)是,依賴(lài)于底層操作系統(tǒng)的 Mutex Lock 實(shí)現(xiàn)。操作系統(tǒng)實(shí)現(xiàn)線(xiàn)程之間的切換矿微,需要從用戶(hù)態(tài)到內(nèi)核態(tài)的切換痕慢,切換成本非常高。
3.4.2 輕量級(jí)鎖
引入輕量級(jí)鎖的主要目的涌矢,是在沒(méi)有多線(xiàn)程競(jìng)爭(zhēng)的前提下掖举,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
當(dāng)關(guān)閉偏向鎖功能或者多個(gè)線(xiàn)程競(jìng)爭(zhēng)偏向鎖娜庇,導(dǎo)致偏向鎖升級(jí)為輕量級(jí)鎖塔次,則會(huì)嘗試獲取輕量級(jí)鎖,其步驟如下:
獲取鎖
判斷當(dāng)前對(duì)象是否處于無(wú)鎖狀態(tài)名秀?若是励负,則 JVM 首先將在當(dāng)前線(xiàn)程的棧幀中,建立一個(gè)名為鎖記錄(Lock Record)的空間匕得,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word的 拷貝(官方把這份拷貝加了一個(gè) Displaced 前綴继榆,即 Displaced Mark Word);否則汁掠,執(zhí)行步驟(3)略吨;
JVM 利用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指正。如果成功考阱,表示競(jìng)爭(zhēng)到鎖翠忠,則將鎖標(biāo)志位變成
00
(表示此對(duì)象處于輕量級(jí)鎖狀態(tài)),執(zhí)行同步操作乞榨;如果失敗负间,則執(zhí)行步驟(3);判斷當(dāng)前對(duì)象的 Mark Word 是否指向當(dāng)前線(xiàn)程的棧幀姜凄?如果是政溃,則表示當(dāng)前線(xiàn)程已經(jīng)持有當(dāng)前對(duì)象的鎖,則直接執(zhí)行同步代碼塊态秧;否則董虱,只能說(shuō)明該鎖對(duì)象已經(jīng)被其他線(xiàn)程搶占了,當(dāng)前線(xiàn)程便嘗試使用自旋來(lái)獲取鎖申鱼。若自旋后沒(méi)有獲得鎖愤诱,此時(shí)輕量級(jí)鎖會(huì)升級(jí)為重量級(jí)鎖,鎖標(biāo)志位變成
10
捐友,當(dāng)前線(xiàn)程會(huì)被阻塞淫半。
釋放鎖
輕量級(jí)鎖的釋放也是通過(guò) CAS 操作來(lái)進(jìn)行的,主要步驟如下:
取出在獲取輕量級(jí)鎖保存在 Displaced Mark Word 中 數(shù)據(jù)匣砖。
使用 CAS 操作將取出的數(shù)據(jù)替換當(dāng)前對(duì)象的 Mark Word 中科吭。如果成功昏滴,則說(shuō)明釋放鎖成功;否則对人,執(zhí)行(3)谣殊。
老艿艿:這塊的描述不太準(zhǔn)確,我的理解是牺弄,無(wú)論(2)是否釋放成功姻几,都會(huì)喚醒被掛起的線(xiàn)程,重新?tīng)?zhēng)奪鎖势告,訪(fǎng)問(wèn)同步代碼塊蛇捌。
下圖是爭(zhēng)奪鎖導(dǎo)致的鎖膨脹的流程圖:
- 其中,綠框的
0
指的是無(wú)偏向鎖咱台,01
指的是無(wú)鎖狀態(tài)络拌。
注意事項(xiàng)
對(duì)于輕量級(jí)鎖,其性能提升的依據(jù)是:“對(duì)于絕大部分的鎖吵护,在整個(gè)生命周期內(nèi)都是不會(huì)存在競(jìng)爭(zhēng)的”。如果打破這個(gè)依據(jù)則除了互斥的開(kāi)銷(xiāo)外表鳍,還有額外的 CAS 操作馅而,因此在有多線(xiàn)程競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖比重量級(jí)鎖更慢譬圣。
3.4.3 偏向鎖
引入偏向鎖主要目的是:為了在無(wú)多線(xiàn)程競(jìng)爭(zhēng)的情況下瓮恭,盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑。
老艿艿:在上文厘熟,我們可以看到偏向鎖時(shí)屯蹦,Mark Word 的數(shù)據(jù)結(jié)構(gòu)為:線(xiàn)程 ID、Epoch( 偏向鎖的時(shí)間戳 )绳姨、對(duì)象分帶年齡登澜、是否是偏向鎖(
1
)、鎖標(biāo)識(shí)位(01
)飘庄。
只需要檢查是否為偏向鎖脑蠕、鎖標(biāo)識(shí)為以及 ThreadID 即可,處理流程如下:
獲取偏向鎖
檢測(cè) Mark Word是 否為可偏向狀態(tài)跪削,即是否為偏向鎖的標(biāo)識(shí)位為
1
谴仙,鎖標(biāo)識(shí)位為01
。若為可偏向狀態(tài)碾盐,則測(cè)試線(xiàn)程 ID 是否為當(dāng)前線(xiàn)程 ID 晃跺?如果是,則執(zhí)行步驟(5)毫玖;否則掀虎,執(zhí)行步驟(3)凌盯。
如果線(xiàn)程 ID 不為當(dāng)前線(xiàn)程 ID ,則通過(guò) CAS 操作競(jìng)爭(zhēng)鎖涩盾。競(jìng)爭(zhēng)成功十气,則將 Mark Word 的線(xiàn)程 ID 替換為當(dāng)前線(xiàn)程 ID ,則執(zhí)行步驟(5)春霍;否則砸西,執(zhí)行線(xiàn)程(4)。
通過(guò) CAS 競(jìng)爭(zhēng)鎖失敗址儒,證明當(dāng)前存在多線(xiàn)程競(jìng)爭(zhēng)情況芹枷,當(dāng)?shù)竭_(dá)全局安全點(diǎn),獲得偏向鎖的線(xiàn)程被掛起莲趣,偏向鎖升級(jí)為輕量級(jí)鎖鸳慈,然后被阻塞在安全點(diǎn)的線(xiàn)程繼續(xù)往下執(zhí)行同步代碼塊。
執(zhí)行同步代碼塊
撤銷(xiāo)偏向鎖
偏向鎖的釋放采用了一種只有競(jìng)爭(zhēng)才會(huì)釋放鎖的機(jī)制喧伞,線(xiàn)程是不會(huì)主動(dòng)去釋放偏向鎖走芋,需要等待其他線(xiàn)程來(lái)競(jìng)爭(zhēng)。
偏向鎖的撤銷(xiāo)需要等待全局安全點(diǎn)(這個(gè)時(shí)間點(diǎn)是上沒(méi)有正在執(zhí)行的代碼)潘鲫。其步驟如下:
暫停擁有偏向鎖的線(xiàn)程翁逞,判斷鎖對(duì)象是否還處于被鎖定狀態(tài)。
撤銷(xiāo)偏向鎖溉仑,恢復(fù)到無(wú)鎖狀態(tài)(
01
)或者輕量級(jí)鎖的狀態(tài)挖函。
老艿艿:關(guān)于偏向鎖的撤銷(xiāo),網(wǎng)上的文章大同小異浊竟,/(ㄒoㄒ)/~~ 看的是一臉懵逼怨喘。
后面擼下源碼,如果擼的動(dòng)振定。
如下是 《Java 8 并發(fā)篇 - 冷靜分析 Synchronized(下)》 對(duì)這塊的描述:
首先會(huì)暫停擁有偏向鎖的線(xiàn)程并檢查該線(xiàn)程是否存活:
- 如果線(xiàn)程非活動(dòng)狀態(tài)必怜,則將對(duì)象頭設(shè)置為無(wú)鎖狀態(tài)(其他線(xiàn)程會(huì)重新獲取該偏向鎖)。
- 如果線(xiàn)程是活動(dòng)狀態(tài)后频,擁有偏向鎖的棧會(huì)被執(zhí)行棚赔,遍歷偏向?qū)ο蟮逆i記錄,并將對(duì)棧中的鎖記錄和對(duì)象頭的 MarkWord 進(jìn)行重置:
* 要么**重新偏向于其他線(xiàn)程**(即將偏向鎖交給其他線(xiàn)程徘郭,相當(dāng)于當(dāng)前線(xiàn)程"被"釋放了鎖) * 要么**恢復(fù)到無(wú)鎖**或者**標(biāo)記鎖對(duì)象不適合作為偏向鎖**(此時(shí)鎖會(huì)被升級(jí)為輕量級(jí)鎖)
最后喚醒暫停的線(xiàn)程靠益,被阻塞在安全點(diǎn)的線(xiàn)程繼續(xù)往下執(zhí)行同步代碼塊
下圖是偏向鎖的獲取和釋放流程:
關(guān)閉偏向鎖
偏向鎖在 JDK 1.6 以上,默認(rèn)開(kāi)啟残揉。開(kāi)啟后程序啟動(dòng)幾秒后才會(huì)被激活胧后,可使用 JVM 參數(shù)
-XX:BiasedLockingStartupDelay = 0
來(lái)關(guān)閉延遲。如果確定鎖通常處于競(jìng)爭(zhēng)狀態(tài)抱环,則可通過(guò)JVM參數(shù)
-XX:-UseBiasedLocking=false
關(guān)閉偏向鎖壳快,那么默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖纸巷。如下是 《Java 8 并發(fā)篇 - 冷靜分析 Synchronized(下)》 對(duì)這塊的描述:
- 優(yōu)勢(shì):偏向鎖只需要在置換 ThreadID 的時(shí)候依賴(lài)一次 CAS 原子指令,其余時(shí)刻不需要 CAS 指令(相比其他鎖)眶痰。
- 隱患:由于一旦出現(xiàn)多線(xiàn)程競(jìng)爭(zhēng)的情況就必須撤銷(xiāo)偏向鎖瘤旨,所以偏向鎖的撤銷(xiāo)操作的性能損耗必須小于節(jié)省下來(lái)的 CAS 原子指令的性能消耗(這個(gè)通常只能通過(guò)大量壓測(cè)才可知)。
- 對(duì)比:輕量級(jí)鎖是為了在線(xiàn)程交替執(zhí)行同步塊時(shí)提高性能竖伯,而偏向鎖則是在只有一個(gè)線(xiàn)程執(zhí)行同步塊時(shí)進(jìn)一步提高性能
3.4.4 對(duì)比和轉(zhuǎn)換
如下是引用自 《Java并發(fā)編程的藝術(shù)》 的對(duì)比圖:
如下是三種鎖之間的轉(zhuǎn)換圖:
如果覺(jué)得解釋不夠清晰的胖友存哲,推薦閱讀占小狼的 《JVM 源碼分析之 synchronized 實(shí)現(xiàn)》 。
參考資料
- 周志明:《深入理解Java虛擬機(jī)》
1. 方騰飛:《Java并發(fā)編程的藝術(shù)》的 [「2.2 synchronized 的實(shí)現(xiàn)原理與引用」](http://www.iocoder.cn/JUC/sike/synchronized/#) 章節(jié)七婴。