為了換取性能,JVM在內(nèi)置鎖上做了非常多的優(yōu)化,膨脹式的鎖分配策略就是其一。理解偏向鎖、輕量級(jí)鎖、重量級(jí)鎖的要解決的基本問題,幾種鎖的分配和膨脹過程,有助于編寫并優(yōu)化基于鎖的并發(fā)程序。
內(nèi)置鎖的分配和膨脹過程較為復(fù)雜,限于時(shí)間和精力,文中該部分內(nèi)容是根據(jù)網(wǎng)上的多方資料整合而來;僅為方便查閱,后面繼續(xù)分析JVM源碼的時(shí)候也有個(gè)參考。如果對(duì)各級(jí)鎖已經(jīng)有了基本了解,讀者大可跳過此文。
隱藏在內(nèi)置鎖下的基本問題
內(nèi)置鎖是JVM提供的最便捷的線程同步工具,在代碼塊或方法聲明上添加synchronized關(guān)鍵字即可使用內(nèi)置鎖。使用內(nèi)置鎖能夠簡(jiǎn)化并發(fā)模型;隨著JVM的升級(jí),幾乎不需要修改代碼,就可以直接享受JVM在內(nèi)置鎖上的優(yōu)化成果。從簡(jiǎn)單的重量級(jí)鎖,到逐漸膨脹的鎖分配策略,使用了多種優(yōu)化手段解決隱藏在內(nèi)置鎖下的基本問題。
重量級(jí)鎖
內(nèi)置鎖在Java中被抽象為監(jiān)視器鎖(monitor)。在JDK 1.6之前,監(jiān)視器鎖可以認(rèn)為直接對(duì)應(yīng)底層操作系統(tǒng)中的互斥量(mutex)。這種同步方式的成本非常高,包括系統(tǒng)調(diào)用引起的內(nèi)核態(tài)與用戶態(tài)切換、線程阻塞造成的線程切換等。因此,后來稱這種鎖為“重量級(jí)鎖”。
自旋鎖
首先,內(nèi)核態(tài)與用戶態(tài)的切換上不容易優(yōu)化。但通過自旋鎖,可以減少線程阻塞造成的線程切換(包括掛起線程和恢復(fù)線程)。
如果鎖的粒度小,那么鎖的持有時(shí)間比較短(盡管具體的持有時(shí)間無法得知,但可以認(rèn)為,通常有一部分鎖能滿足上述性質(zhì))。那么,對(duì)于競(jìng)爭(zhēng)這些鎖的而言,因?yàn)殒i阻塞造成線程切換的時(shí)間與鎖持有的時(shí)間相當(dāng),減少線程阻塞造成的線程切換,能得到較大的性能提升。具體如下:
- 當(dāng)前線程競(jìng)爭(zhēng)鎖失敗時(shí),打算阻塞自己
- 不直接阻塞自己,而是自旋(空等待,比如一個(gè)空的有限for循環(huán))一會(huì)
- 在自旋的同時(shí)重新競(jìng)爭(zhēng)鎖
- 如果自旋結(jié)束前獲得了鎖,那么鎖獲取成功;否則,自旋結(jié)束后阻塞自己
如果在自旋的時(shí)間內(nèi),鎖就被舊owner釋放了,那么當(dāng)前線程就不需要阻塞自己(也不需要在未來鎖釋放時(shí)恢復(fù)),減少了一次線程切換。
“鎖的持有時(shí)間比較短”這一條件可以放寬。實(shí)際上,只要鎖競(jìng)爭(zhēng)的時(shí)間比較短(比如線程1快釋放鎖的時(shí)候,線程2才會(huì)來競(jìng)爭(zhēng)鎖),就能夠提高自旋獲得鎖的概率。這通常發(fā)生在鎖持有時(shí)間長(zhǎng),但競(jìng)爭(zhēng)不激烈的場(chǎng)景中。
缺點(diǎn)
- 單核處理器上,不存在實(shí)際的并行,當(dāng)前線程不阻塞自己的話,舊owner就不能執(zhí)行,鎖永遠(yuǎn)不會(huì)釋放,此時(shí)不管自旋多久都是浪費(fèi);進(jìn)而,如果線程多而處理器少,自旋也會(huì)造成不少無謂的浪費(fèi)。
- 自旋鎖要占用CPU,如果是計(jì)算密集型任務(wù),這一優(yōu)化通常得不償失,減少鎖的使用是更好的選擇。
- 如果鎖競(jìng)爭(zhēng)的時(shí)間比較長(zhǎng),那么自旋通常不能獲得鎖,白白浪費(fèi)了自旋占用的CPU時(shí)間。這通常發(fā)生在鎖持有時(shí)間長(zhǎng),且競(jìng)爭(zhēng)激烈的場(chǎng)景中,此時(shí)應(yīng)主動(dòng)禁用自旋鎖。
使用-XX:-UseSpinning參數(shù)關(guān)閉自旋鎖優(yōu)化;-XX:PreBlockSpin參數(shù)修改默認(rèn)的自旋次數(shù)。
自適應(yīng)自旋鎖
自適應(yīng)意味著自旋的時(shí)間不再固定了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定:
- 如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間,比如100個(gè)循環(huán)。
- 相反的,如果對(duì)于某個(gè)鎖,自旋很少成功獲得過,那在以后要獲取這個(gè)鎖時(shí)將可能減少自旋時(shí)間甚至省略自旋過程,以避免浪費(fèi)處理器資源。
自適應(yīng)自旋解決的是“鎖競(jìng)爭(zhēng)時(shí)間不確定”的問題。JVM很難感知到確切的鎖競(jìng)爭(zhēng)時(shí)間,而交給用戶分析就違反了JVM的設(shè)計(jì)初衷。自適應(yīng)自旋假定不同線程持有同一個(gè)鎖對(duì)象的時(shí)間基本相當(dāng),競(jìng)爭(zhēng)程度趨于穩(wěn)定,因此,可以根據(jù)上一次自旋的時(shí)間與結(jié)果調(diào)整下一次自旋的時(shí)間。
缺點(diǎn)
然而,自適應(yīng)自旋也沒能徹底解決該問題,如果默認(rèn)的自旋次數(shù)設(shè)置不合理(過高或過低),那么自適應(yīng)的過程將很難收斂到合適的值。
輕量級(jí)鎖
自旋鎖的目標(biāo)是降低線程切換的成本。如果鎖競(jìng)爭(zhēng)激烈,我們不得不依賴于重量級(jí)鎖,讓競(jìng)爭(zhēng)失敗的線程阻塞;如果完全沒有實(shí)際的鎖競(jìng)爭(zhēng),那么申請(qǐng)重量級(jí)鎖都是浪費(fèi)的。輕量級(jí)鎖的目標(biāo)是,減少無實(shí)際競(jìng)爭(zhēng)情況下,使用重量級(jí)鎖產(chǎn)生的性能消耗,包括系統(tǒng)調(diào)用引起的內(nèi)核態(tài)與用戶態(tài)切換、線程阻塞造成的線程切換等。
顧名思義,輕量級(jí)鎖是相對(duì)于重量級(jí)鎖而言的。使用輕量級(jí)鎖時(shí),不需要申請(qǐng)互斥量,僅僅將Mark Word中的部分字節(jié)CAS更新指向線程棧中的Lock Record,如果更新成功,則輕量級(jí)鎖獲取成功,記錄鎖狀態(tài)為輕量級(jí)鎖;否則,說明已經(jīng)有線程獲得了輕量級(jí)鎖,目前發(fā)生了鎖競(jìng)爭(zhēng)(不適合繼續(xù)使用輕量級(jí)鎖),接下來膨脹為重量級(jí)鎖。
Mark Word是對(duì)象頭的一部分;每個(gè)線程都擁有自己的線程棧(虛擬機(jī)棧),記錄線程和函數(shù)調(diào)用的基本信息。二者屬于JVM的基礎(chǔ)內(nèi)容,此處不做介紹。
當(dāng)然,由于輕量級(jí)鎖天然瞄準(zhǔn)不存在鎖競(jìng)爭(zhēng)的場(chǎng)景,如果存在鎖競(jìng)爭(zhēng)但不激烈,仍然可以用自旋鎖優(yōu)化,自旋失敗后再膨脹為重量級(jí)鎖。
缺點(diǎn)
同自旋鎖相似:
- 如果鎖競(jìng)爭(zhēng)激烈,那么輕量級(jí)將很快膨脹為重量級(jí)鎖,那么維持輕量級(jí)鎖的過程就成了浪費(fèi)。
偏向鎖
在沒有實(shí)際競(jìng)爭(zhēng)的情況下,還能夠針對(duì)部分場(chǎng)景繼續(xù)優(yōu)化。如果不僅僅沒有實(shí)際競(jìng)爭(zhēng),自始至終,使用鎖的線程都只有一個(gè),那么,維護(hù)輕量級(jí)鎖都是浪費(fèi)的。偏向鎖的目標(biāo)是,減少無競(jìng)爭(zhēng)且只有一個(gè)線程使用鎖的情況下,使用輕量級(jí)鎖產(chǎn)生的性能消耗。輕量級(jí)鎖每次申請(qǐng)、釋放鎖都至少需要一次CAS,但偏向鎖只有初始化時(shí)需要一次CAS。
“偏向”的意思是,偏向鎖假定將來只有第一個(gè)申請(qǐng)鎖的線程會(huì)使用鎖(不會(huì)有任何線程再來申請(qǐng)鎖),因此,只需要在Mark Word中CAS記錄owner(本質(zhì)上也是更新,但初始值為空),如果記錄成功,則偏向鎖獲取成功,記錄鎖狀態(tài)為偏向鎖,以后當(dāng)前線程等于owner就可以零成本的直接獲得鎖;否則,說明有其他線程競(jìng)爭(zhēng),膨脹為輕量級(jí)鎖。
偏向鎖無法使用自旋鎖優(yōu)化,因?yàn)橐坏┯衅渌€程申請(qǐng)鎖,就破壞了偏向鎖的假定。
缺點(diǎn)
同樣的,如果明顯存在其他線程申請(qǐng)鎖,那么偏向鎖將很快膨脹為輕量級(jí)鎖。
不過這個(gè)副作用已經(jīng)小的多。
如果需要,使用參數(shù)-XX:-UseBiasedLocking禁止偏向鎖優(yōu)化(默認(rèn)打開)。
小結(jié)
偏向鎖、輕量級(jí)鎖、重量級(jí)鎖分配和膨脹的詳細(xì)過程見后。會(huì)涉及一些Mark Word與CAS的知識(shí)。
偏向鎖、輕量級(jí)鎖、重量級(jí)鎖適用于不同的并發(fā)場(chǎng)景:
- 偏向鎖:無實(shí)際競(jìng)爭(zhēng),且將來只有第一個(gè)申請(qǐng)鎖的線程會(huì)使用鎖。
- 輕量級(jí)鎖:無實(shí)際競(jìng)爭(zhēng),多個(gè)線程交替使用鎖;允許短時(shí)間的鎖競(jìng)爭(zhēng)。
- 重量級(jí)鎖:有實(shí)際競(jìng)爭(zhēng),且鎖競(jìng)爭(zhēng)時(shí)間長(zhǎng)。
另外,如果鎖競(jìng)爭(zhēng)時(shí)間短,可以使用自旋鎖進(jìn)一步優(yōu)化輕量級(jí)鎖、重量級(jí)鎖的性能,減少線程切換。
如果鎖競(jìng)爭(zhēng)程度逐漸提高(緩慢),那么從偏向鎖逐步膨脹到重量鎖,能夠提高系統(tǒng)的整體性能。
鎖分配和膨脹過程
重申,這部分主要是根據(jù)網(wǎng)上的多方資料整理。核心是這位巨巨整理的流程圖,相當(dāng)詳細(xì),基本符合邏輯。
前面講述了內(nèi)置鎖在使用過程中的一些基本問題和解決方案,實(shí)現(xiàn)原理一筆帶過。詳細(xì)的鎖分配和膨脹過程如下:

圖中有一處疑問:
按照?qǐng)D中流程,如果發(fā)現(xiàn)鎖已經(jīng)膨脹為重量級(jí)鎖,就直接使用互斥量mutex阻塞當(dāng)前線程。
然而,自旋鎖的一大好處就是減少線程切換的開銷。在這里沒有必要直接阻塞當(dāng)前線程,大可以像輕量級(jí)鎖一樣,自旋一會(huì),失敗了再阻塞。
特別說明兩點(diǎn):
- CAS記錄owner時(shí),
expected == null,newValue == ownerThreadId,因此,只有第一個(gè)申請(qǐng)偏向鎖的線程能夠返回成功,后續(xù)線程都必然失敗(部分線程檢測(cè)到可偏向,同時(shí)嘗試CAS記錄owner)。 - 內(nèi)置鎖只能沿著偏向鎖、輕量級(jí)鎖、重量級(jí)鎖的順序逐漸膨脹,不能“收縮”。這基于JVM的另一個(gè)假定,“一旦破壞了上一級(jí)鎖的假定,就認(rèn)為該假定以后也必不成立”。
另外,當(dāng)重量級(jí)鎖被解除后,需要喚醒一個(gè)被阻塞的線程,這部分邏輯與ReentrantLock基本相同,詳見源碼|并發(fā)一枝花之ReentrantLock與AQS(1):lock、unlock。
簡(jiǎn)化版
上圖記載的很詳細(xì),也有Mark Word的圖解。看懂上圖后,再來看《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐(第2版)》中的簡(jiǎn)化版流程圖就能看懂了:

挖坑:
簡(jiǎn)化版中指出了
重偏向過程。這一過程對(duì)于性能優(yōu)化和膨脹過程都非常重要;但如果考慮重偏向的話,可能上述特別說明的內(nèi)容就不成立了。要整理的筆記太多啦時(shí)間不夠啊,猴子選擇暫時(shí)放棄這個(gè)問題,,,恩,挖個(gè)坑,以后再追源碼填坑。重偏向和epoch的作用參考:
參考:
本文鏈接:淺談偏向鎖、輕量級(jí)鎖、重量級(jí)鎖
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識(shí)共享署名-相同方式共享 4.0 國(guó)際許可協(xié)議發(fā)布,歡迎轉(zhuǎn)載,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接。