基礎(chǔ)知識
線程切換代價
Java的線程是映射到操作系統(tǒng)的原生線程之上的,如果阻塞或喚醒一個線程就需要操作系統(tǒng)介入砖茸,需要在用戶態(tài)和內(nèi)核態(tài)之間切換隘擎,該切換會消耗大量的系統(tǒng)資源,因為用戶態(tài)和內(nèi)核態(tài)均有各自專用的內(nèi)存空間凉夯,專用的寄存器等货葬,用戶態(tài)切換至內(nèi)核態(tài)需要傳遞很多變量、參數(shù)給內(nèi)核劲够,內(nèi)核也需要保護好用戶態(tài)切換時的一些寄存器值震桶、變量等,以便內(nèi)核態(tài)調(diào)用結(jié)束后切換回用戶態(tài)繼續(xù)工作征绎。
JVM1.6之前蹲姐,Synchronized會導致爭不到鎖的線程直接進入阻塞狀態(tài),所以說其是一個重量級的同步操作,被稱為重量鎖柴墩。
為了緩解上述的性能問題忙厌,JVM1.6開始,引入了偏向鎖江咳、輕量鎖逢净,其均屬于樂觀鎖。
Mark Word
在JVM 1.6中歼指,對象實例在堆內(nèi)存中被分為3部分: 對象頭爹土、實例數(shù)據(jù)、對齊填充踩身。
對象頭的組成部分: Mark Word着饥、指向類的指針、數(shù)組長度(可選惰赋,數(shù)組類型時才有)宰掉,每個部分長度均為1個字寬,32位的JVM中赁濒,1字寬位32 bit轨奄,64位的JVM中,1字寬為64 bit拒炎。
鎖升級功能主要依賴Mark Word中鎖標志位和是否偏向鎖標志位挪拟。
Synchronized同步鎖的升級優(yōu)化路徑: 偏向鎖->輕量級鎖->重量級鎖。
鎖升級
偏向鎖
偏向鎖主要用來優(yōu)化同一線程多次申請同一個鎖的競爭击你,在某些情況下玉组,大部分時間都是同一個線程競爭鎖資源。
當線程1再次獲取鎖時丁侄,會比較當前線程ID與鎖對象頭Mark Word中的線程ID是否一致惯雳。
- 如果一致,直接獲取鎖鸿摇,無需CAS來搶占鎖石景;
- 如果不一致,需要查看鎖對象頭Mark Word中的線程是否存活:
- 若存活拙吉,查找線程1的棧幀信息潮孽,如果線程1還需要繼續(xù)持有該鎖對象,那么暫停線程1(Stop-The-World)筷黔,撤銷偏向鎖往史,升級為輕量級鎖;如果線程1不再使用鎖對象佛舱,則將鎖對象設(shè)置為無鎖狀態(tài)(也屬于鎖撤銷)椎例,然后重新偏向線程2揽乱;
- 若不存活,則將鎖對象設(shè)置為無鎖狀態(tài)(也屬于鎖撤銷)粟矿,然后重新偏向線程2。
可以看到损拢,當持有鎖的線程宕掉之后陌粹,其他請求鎖的線程會檢查持有鎖的線程是否存活,若不存活則直接撤銷鎖福压,從而避免了死鎖掏秩。
在高并發(fā)場景下,當大量線程同時競爭同一個鎖資源時荆姆,偏向鎖會被撤銷蒙幻,發(fā)生STW,加大性能開銷胆筒。
JVM的默認配置為: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000邮破,即默認開啟偏向鎖,并且延遲4秒生效仆救,之所以延遲抒和,是因為JVM剛啟動時競爭比較激烈。
關(guān)閉偏向鎖: -XX:-UseBiasedLocking彤蔽,也可以直接設(shè)置為重量級鎖: -XX:+UseHeavyMonitors摧莽。
輕量鎖
輕量鎖適應的場景是: 各線程交替執(zhí)行同步塊,大部分的鎖在同步周期內(nèi)不存在長時間的競爭顿痪。
輕量鎖在虛擬機內(nèi)部是通過BasicObjectLock對象實現(xiàn)的镊辕,該對象內(nèi)部由一個BasicLock對象_lock和一個鎖對象指針_obj組成,BasicObjectLock對象放置在Java棧幀中蚁袭。
在BasicLock內(nèi)部還維護著displace_header字段征懈,用于備份鎖對象頭部的Mark Word。
// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
private:
BasicLock _lock; // the lock, must be double word aligned
oop _obj; // object holds the lock;
};
class BasicLock VALUE_OBJ_CLASS_SPEC {
private:
volatile markOop _displaced_header;
};
當需要判斷一個線程是否持有該鎖對象時揩悄,只需要簡單判斷鎖對象頭的指針是否在當前線程棧地址范圍即可受裹。
加鎖
線程在執(zhí)行同步塊之前,JVM會先在當前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間虏束,即BasicObjectLock對象棉饶。
創(chuàng)建過程如下:
- 將鎖對象頭的Mark Word拷貝賦值給BasicObjectLock中BasicLock對象的_displaced_header字段;
- 然后線程嘗試使用CAS將鎖對象頭的Mark Word替換為指向該BasicObjectLock對象的指針镇匀;
- 若成功照藻,則當前線程獲得鎖。若失敗汗侵,表示其他線程競爭鎖幸缕,當前線程嘗試使用自旋來獲取鎖群发。
可以看到,當前線程即時獲取鎖失敗发乔,也不會立即阻塞掛起熟妓,而是先嘗試使用自旋來獲取鎖。如果競爭不是很激烈栏尚,可能幾次自旋起愈,當前線程就獲取到鎖了,從而避免了1次線程的上下文切換译仗。
若當前線程自旋獲取鎖失敗抬虽,鎖就會膨脹成重量鎖,當前線程阻塞掛起纵菌。
解鎖
輕量鎖解鎖時阐污,會使用原子的CAS操作將BasicObjectLock對象備份的_displaced_header替換回到鎖對象頭的Mark Word。若成功咱圆,則表明沒有競爭發(fā)生笛辟,解鎖成功;若失敗序苏,則表明當前鎖存在競爭(此時鎖已經(jīng)膨脹為重量鎖)隘膘,釋放鎖并喚醒阻塞的線程。
自旋鎖
當鎖處于輕量鎖狀態(tài)杠览,且被某線程持有時弯菊,其他線程嘗試獲取鎖失敗后,不會直接阻塞掛起踱阿,而是先自旋一定次數(shù)管钳,避免正在持有鎖的線程可能在很短的時間內(nèi)釋放鎖資源。
從JVM 1.7開始软舌,自旋鎖默認啟用才漆,自旋次數(shù)不宜設(shè)置過大(避免長時間占用CPU),-XX:+UseSpinning -XX:PreBlockSpin=10佛点,JVM 1.7之后醇滥,默認的自旋次數(shù)由JVM根據(jù)實際系統(tǒng)環(huán)境靈活設(shè)置。
在鎖競爭不是很激烈且鎖占用的時間非常短的場景下超营,自旋鎖可以通過減少上下文切換來提高系統(tǒng)性能鸳玩;在鎖競爭激烈或者鎖占用時間較長的場景下,自旋鎖會導致大量的線程一直處于CAS重試狀態(tài)演闭,造成CPU空轉(zhuǎn)不跟。
在高并發(fā)場景下,可以通過關(guān)閉自旋鎖來優(yōu)化系統(tǒng)性能: -XX:-UseSpinning米碰。
該線程自旋之后仍舊未獲取鎖窝革,則其會將鎖對象升級為重量鎖购城。未搶到鎖的線程都會進入Monitor,之后會被阻塞到WaitSet中虐译。
這里有個問題瘪板,假設(shè)持有輕量鎖的線程執(zhí)行同步塊的時候宕掉了,則不會有釋放鎖并喚醒阻塞線程的動作漆诽,此時會造成死鎖嗎侮攀?
答案是肯定不會的,因為其他線程請求鎖時拴泌,會有1個線程自旋獲取鎖失敗后將鎖升級為重量鎖,然后獲取重量鎖的線程在釋放的時候會將WaitSet中阻塞的線程喚醒惊橱,也就是說即使持有輕量級鎖的線程不喚醒阻塞線程蚪腐,其他持有重量級鎖的線程在釋放鎖的時候也會喚醒WaitSet中的阻塞線程的,可謂是雙重保障税朴,哈哈回季。
重量鎖
當多個線程同時請求某個Monitor時,對象的Monitor會設(shè)置以下狀態(tài)用來區(qū)分請求的線程:
- Contention List: 所有請求鎖的線程被首先放置到該競爭隊列正林;
- Entry List: Contention List中那些有資格成為候選人的線程會被轉(zhuǎn)移到Entry List泡一;
- Wait Set: 調(diào)用Wait方法等被阻塞的線程會被放置到Wait Set;
- OnDeck: 任何時刻僅能有1個線程競爭鎖觅廓,該線程稱為OnDeck鼻忠;
- Owner: 獲取鎖的線程稱為Owner;
- !Owner: 釋放鎖的線程杈绸。
EntryList與ContentionList邏輯上同屬等待隊列帖蔓,ContentionList會被線程并發(fā)訪問,為了降低對ContentionList隊尾的爭用瞳脓,而建立EntryList塑娇。Owner線程在unlock時會從ContentionList中遷移線程到EntryList,并會指定EntryList中的某個線程(一般為Head)為Ready(OnDeck)線程劫侧。Owner線程并不是把鎖傳遞給OnDeck線程埋酬,只是把競爭鎖的權(quán)利交給OnDeck,OnDeck線程需要重新競爭鎖烧栋。這樣做雖然犧牲了一定的公平性写妥,但極大的提高了整體吞吐量,在 Hotspot中把OnDeck的選擇行為稱之為“競爭切換”审姓。
OnDeck線程獲得鎖后即變?yōu)镺wner線程耳标,無法獲得鎖則會依然留在EntryList中,考慮到公平性邑跪,在EntryList中的位置不發(fā)生變化(依然在隊頭)次坡。如果Owner線程被wait方法阻塞呼猪,則轉(zhuǎn)移到WaitSet隊列;如果在某個時刻被notify/notifyAll喚醒砸琅,則再次轉(zhuǎn)移到EntryList宋距。
需要注意的是:當持有重量鎖的線程在運行期間出錯,會自動釋放掉鎖症脂,從而避免死鎖谚赎。
小結(jié)
鎖升級的過程可以通過下面2張圖來展示: