前言
本篇文章介紹Java Synchronized鎖優(yōu)化番甩。
- 鎖是存在哪里的瓮顽,怎么標(biāo)識是什么鎖
- Monitor機制在Java中怎么表現(xiàn)的
- 鎖優(yōu)化
- 鎖升級
1. 鎖存在哪里
- 對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭铁追、實例數(shù)據(jù)和對齊填充驱证。
Hotspot虛擬機的對象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)蜻势、Klass Pointer(類型指針)等限,數(shù)組會多1字寬(32位: 4字節(jié))來存儲數(shù)組長度有巧。 - synchronized用的鎖是存在Java對象頭里的释漆。
其中Klass Point是對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例篮迎;Mark Word用于存儲對象自身的運行時數(shù)據(jù)男图,它是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵示姿。
JVM中對象頭的方式有以下兩種(以32位JVM為例):
// 普通對象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
// 數(shù)組對象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
- Mark Word
這部分主要用來存儲對象自身的運行時數(shù)據(jù),如hashcode逊笆、gc分代年齡栈戳、鎖狀態(tài)標(biāo)志、線程持有的鎖难裆、偏向線程 ID子檀、偏向時間戳等等。
mark word的位長度為JVM的一個Word大小乃戈,也就是說32位JVM的Mark word為32位褂痰,64位JVM為64位。
Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù)症虑,它會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間缩歪,為了讓一個字大小存儲更多的信息,JVM將字的最低兩個位設(shè)置為標(biāo)記位谍憔,不同標(biāo)記位下的Mark Word示意如下:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:0 |lock:01 | Normal無鎖 |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1| lock:01 | Biased偏向鎖 |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:00 | Lightweight Locked輕量級鎖 |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:10 | Heavyweight Locked重量級鎖 |
|-------------------------------------------------------|--------------------|
| | lock:11 | Marked for GC GC標(biāo)記|
|-------------------------------------------------------|--------------------|
- lock:2位的鎖狀態(tài)標(biāo)記位匪蝙,由于希望用盡可能少的二進制位表示盡可能多的信息,所以設(shè)置了lock標(biāo)記习贫。該標(biāo)記的值不同逛球,整個mark word表示的含義不同。
- biased_lock:對象是否啟用偏向鎖標(biāo)記苫昌,只占1個二進制位颤绕。為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖蜡歹。
- age:4位的Java對象年齡屋厘。在GC中涕烧,如果對象在Survivor區(qū)復(fù)制一次月而,年齡增加1。當(dāng)對象達(dá)到設(shè)定的閾值時议纯,將會晉升到老年代父款。默認(rèn)情況下,并行GC的年齡閾值為15瞻凤,并發(fā)GC的年齡閾值為6憨攒。由于age只有4位,所以最大值為15阀参,這就是-XX:MaxTenuringThreshold選項最大值為15的原因肝集。
- identity_hashcode:25位的對象標(biāo)識Hash碼,采用延遲加載技術(shù)蛛壳。調(diào)用方法System.identityHashCode()計算杏瞻,并會將結(jié)果寫到該對象頭中所刀。當(dāng)對象被鎖定時,該值會移動到管程Monitor中捞挥。
- thread:持有偏向鎖的線程ID浮创。
- epoch:偏向時間戳。
- ptr_to_lock_record:指向棧中鎖記錄的指針砌函。
- ptr_to_heavyweight_monitor:指向管程Monitor的指針斩披。
2. Monitor機制
Monitor其實是一種同步工具、同步機制讹俊,在Java中垦沉,Object 類本身就是監(jiān)視者對象,Java 對于 Monitor Object 模式做了內(nèi)建的支持仍劈,即每一個Java對象是天生的Monitor乡话,每一個Java對象都有成為Monitor的潛質(zhì)。
并且同時只能有一個線程可以獲得該對象monitor的所有權(quán)耳奕。在線程進入時通過monitorenter嘗試取得對象monitor所有權(quán)绑青,退出時通過monitorexit釋放對象monitor所有權(quán)。
- monitorenter過程如下:
如果 monitor 的進入數(shù)為0屋群,則該線程進入 monitor闸婴,然后將進入數(shù)設(shè)置為1,該線程即為 monitor 的所有者芍躏;
如果線程已經(jīng)占有monitor邪乍,只是重新進入,則monitor的進入數(shù)+1对竣;
如果其他線程已經(jīng)占用 monitor庇楞,則該線程處于阻塞狀態(tài),直至 monitor 的進入數(shù)為0否纬,再重新嘗試獲得 monitor 的所有權(quán) - monitorexit:
執(zhí)行 monitorexit 的線程必須是 objectref 所對應(yīng)的 monitor 的所有者吕晌。執(zhí)行指令時,monitor 的進入數(shù)減1临燃,如果減1后進入數(shù)為0睛驳,則線程退出 monitor,不再是這個 monitor 的所有者膜廊,其他被這個 monitor 阻塞的線程可以嘗試獲取這個 monitor 的所有權(quán)乏沸。
Monitor 是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個線程都有一個可用monitor record列表爪瓜,同時還有一個全局的可用列表蹬跃。
每一個被鎖住的對象都會和一個monitor關(guān)聯(lián)(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標(biāo)識铆铆,表示該鎖被這個線程占用蝶缀。
- Owner:初始時為NULL表示當(dāng)前沒有任何線程擁有該monitor record辆苔,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識,當(dāng)鎖被釋放時又設(shè)置為NULL扼劈;
- EntryQ:關(guān)聯(lián)一個系統(tǒng)互斥鎖(semaphore)驻啤,阻塞所有試圖鎖住monitor record失敗的線程。
- RcThis:表示blocked或waiting在該monitor record上的所有線程的個數(shù)荐吵。
- Nest:用來實現(xiàn)重入鎖的計數(shù)骑冗。
- HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
- Candidate:用來避免不必要的阻塞或等待線程喚醒先煎,因為每一次只有一個線程能夠成功擁有鎖贼涩,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導(dǎo)致性能嚴(yán)重下降薯蝎。Candidate只有兩種可能的值遥倦,0表示沒有需要喚醒的線程,1表示要喚醒一個繼任線程來競爭鎖占锯。
3. 鎖優(yōu)化
jdk1.6對鎖的實現(xiàn)引入了大量的優(yōu)化袒哥,如自旋鎖、適應(yīng)性自旋鎖消略、鎖消除堡称、鎖粗化、偏向鎖艺演、輕量級鎖等技術(shù)來減少鎖操作的開銷却紧。
鎖主要存在四中狀態(tài),依次是:無鎖狀態(tài)胎撤、偏向鎖狀態(tài)晓殊、輕量級鎖狀態(tài)、重量級鎖狀態(tài)伤提,他們會隨著競爭的激烈而逐漸升級巫俺。
其中鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率飘弧。
1. 重量級鎖
monitor 監(jiān)視器鎖本質(zhì)上是依賴操作系統(tǒng)的 Mutex Lock 互斥量 來實現(xiàn)的识藤,我們一般稱之為重量級鎖砚著。因為 OS 實現(xiàn)線程間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)次伶,這個轉(zhuǎn)換過程成本較高,耗時相對較長稽穆,因此 synchronized 效率會比較低冠王。
2. 輕量級鎖
輕量級鎖,其性能提升的依據(jù)是對于絕大部分的鎖舌镶,在整個生命周期內(nèi)都是不會存在競爭的
柱彻,如果沒有競爭豪娜,輕量級鎖就可以使用 CAS 操作避免互斥量的開銷,從而提升效率哟楷。
如果打破這個依據(jù)則除了互斥的開銷外瘤载,還有額外的CAS操作,因此在有多線程競爭的情況下卖擅,輕量級鎖比重量級鎖更慢鸣奔。
-
輕量級鎖的加鎖過程:
-
線程在進入到同步代碼塊的時候,JVM 會先在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間惩阶,用于存儲鎖對象當(dāng)前 Mark Word 的拷貝(官方稱為 Displaced Mark Word)挎狸,owner 指針指向?qū)ο蟮?Mark Word。此時堆棧與對象頭的狀態(tài)如圖所示:
JVM 使用 CAS 操作嘗試將對象頭中的 Mark Word 更新為指向 Lock Record 的指針断楷。如果更新成功锨匆,則執(zhí)行步驟3;更新失敗冬筒,則執(zhí)行步驟4
-
如果更新成功恐锣,那么這個線程就擁有了該對象的鎖,對象的 Mark Word 的鎖狀態(tài)為輕量級鎖(標(biāo)志位轉(zhuǎn)變?yōu)?00')舞痰。此時線程堆棧與對象頭的狀態(tài)如圖所示:
如果更新失敗侥蒙,JVM 首先檢查對象的 Mark Word 是否指向當(dāng)前線程的棧幀
如果是,就說明當(dāng)前線程已經(jīng)擁有了該對象的鎖匀奏,那就可以直接進入同步代碼塊繼續(xù)執(zhí)行
如果不是鞭衩,就說明這個鎖對象已經(jīng)被其他的線程搶占了,當(dāng)前線程會嘗試自旋一定次數(shù)來獲取鎖娃善。如果自旋一定次數(shù) CAS 操作仍沒有成功论衍,那么輕量級鎖就要升級為重量級鎖(鎖的標(biāo)志位轉(zhuǎn)變?yōu)?10'),Mark Word 中存儲的就是指向重量級鎖的指針聚磺,后面等待鎖的線程也就進入阻塞狀態(tài)
-
-
輕量級鎖的解鎖過程:
- 通過 CAS 操作用線程中復(fù)制的 Displaced Mark Word 中的數(shù)據(jù)替換對象當(dāng)前的 Mark Word
- 如果替換成功坯台,整個同步過程就完成了
- 如果替換失敗,說明有其他線程嘗試過獲取該鎖瘫寝,那就在釋放鎖的同時蜒蕾,喚醒被掛起的線程
3. 偏向鎖
依據(jù):對于絕大部分鎖,在整個同步周期內(nèi)不僅不存在競爭焕阿,而且總由同一線程多次獲得咪啡。
在一些情況下總是同一線程多次獲得鎖,此時第二次再重新做CAS修改對象頭中的Mark Word這樣的操作暮屡,有些多余撤摸。所以就有了偏向鎖,只需要檢查是否為偏向鎖、鎖標(biāo)識為以及ThreadID即可准夷,只要是同一線程就不再修改對象頭钥飞。其目的為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑。
-
偏向鎖枷鎖過程:
- 檢測Mark Word是否為可偏向狀態(tài)衫嵌,即是否為偏向鎖1读宙,鎖標(biāo)識位為01;
- 若為可偏向狀態(tài)楔绞,則測試線程ID是否為當(dāng)前線程ID论悴,如果是,則執(zhí)行步驟(5)墓律,否則執(zhí)行步驟(3)膀估;
- 如果線程ID不為當(dāng)前線程ID,則通過CAS操作競爭鎖耻讽,競爭成功察纯,則將Mark Word的線程ID替換為當(dāng)前線程ID,否則執(zhí)行線程(4)针肥;
- 通過CAS競爭鎖失敗饼记,證明當(dāng)前存在多線程競爭情況,當(dāng)?shù)竭_(dá)全局安全點慰枕,獲得偏向鎖的線程被掛起具则,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼塊具帮;
- 執(zhí)行同步代碼塊
-
偏向鎖釋放過程:
- 當(dāng)一個線程已經(jīng)持有偏向鎖博肋,而另外一個線程嘗試競爭偏向鎖時,CAS 替換 ThreadID 操作失敗蜂厅,則開始撤銷偏向鎖匪凡。偏向鎖的撤銷,需要等待原持有偏向鎖的線程到達(dá)全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行)掘猿,暫停該線程病游,并檢查其狀態(tài)
- 如果原持有偏向鎖的線程不處于活動狀態(tài)或已退出同步代碼塊,則該線程釋放鎖稠通。將對象頭設(shè)置為無鎖狀態(tài)(鎖標(biāo)志位為'01'衬衬,是否偏向標(biāo)志位為'0')
- 如果原持有偏向鎖的線程未退出同步代碼塊,則升級為輕量級鎖(鎖標(biāo)志位為'00')
4. 總結(jié)
鎖主要存在四中狀態(tài)改橘,依次是:無鎖狀態(tài)滋尉、偏向鎖狀態(tài)、輕量級鎖狀態(tài)唧龄、重量級鎖狀態(tài)兼砖,其升級如下圖所示:
其他優(yōu)化
自旋鎖:
互斥同步時奸远,掛起和恢復(fù)線程都需要切換到內(nèi)核態(tài)完成既棺,這對性能并發(fā)帶來了不少的壓力讽挟。同時在許多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間丸冕,為了這段較短的時間而去掛起和恢復(fù)線程并不值得耽梅。那么如果有多個線程同時并行執(zhí)行,可以讓后面請求鎖的線程通過自旋(CPU忙循環(huán)執(zhí)行空指令)的方式稍等一會兒胖烛,看看持有鎖的線程是否會很快的釋放鎖眼姐,這樣就不需要放棄 CPU 的執(zhí)行時間了。適應(yīng)性自旋:
自旋時如果鎖被占用的時間比較短佩番,那么自旋等待的效果就會比較好众旗,而如果鎖占用的時間很長,自旋的線程則會白白浪費 CPU 資源趟畏。解決這個問題的最簡答的辦法就是:指定自旋的次數(shù)贡歧,如果在限定次數(shù)內(nèi)還沒獲取到鎖(例如10次),就按傳統(tǒng)的方式掛起線程進入阻塞狀態(tài)赋秀。
JDK1.6 之后引入了自適應(yīng)性自旋的方式利朵,如果在同一鎖對象上,一線程自旋等待剛剛成功獲得鎖猎莲,并且持有鎖的線程正在運行中绍弟,那么 JVM 會認(rèn)為這次自旋也有可能再次成功獲得鎖,進而允許自旋等待相對更長的時間(例如100次)著洼。另一方面樟遣,如果某個鎖自旋很少成功獲得,那么以后要獲得這個鎖時將省略自旋過程身笤,以避免浪費 CPU年碘。鎖消除
虛擬機即時編譯器(JIT)運行時,依據(jù)逃逸分析的數(shù)據(jù)檢測到不可能存在競爭的鎖展鸡,就自動將該鎖消除)屿衅。鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持。
如果判斷一段代碼中莹弊,堆上的數(shù)據(jù)不會逃逸出去從而被其他線程訪問到涤久,則可以把他們當(dāng)做棧上的數(shù)據(jù)對待,認(rèn)為它們是線程私有的忍弛,不必要加鎖响迂。
如下所示,在 StringBuffer.append() 方法中有一個同步代碼塊细疚,鎖就是sb對象蔗彤,但 sb 的所有引用不會逃逸到 concatString() 方法外部,其他線程無法訪問它。因此這里有鎖然遏,但是在即時編譯之后贫途,會被安全的消除掉,忽略掉同步而直接執(zhí)行了待侵。
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
return sb.toString();
}
- 鎖粗化
鎖粗化就是 JVM 檢測到一串零碎的操作都對同一個對象加鎖丢早,則會把加鎖同步的范圍粗化到整個操作序列的外部。
以上述 concatString() 方法為例秧倾,內(nèi)部的 StringBuffer.append() 每次都會加鎖怨酝,將會鎖粗化,在第一次 append() 前至 最后一個 append() 后只需要加一次鎖就可以了那先。
結(jié)語
本篇文章介紹了JVM對Synchronized進行的鎖優(yōu)化农猬。自旋到自適應(yīng)自旋、鎖消除和鎖粗化售淡,從無鎖到偏向鎖斤葱、輕量級鎖,都在避免線程進入內(nèi)核態(tài)進行的切換勋又。針對各種情況做的優(yōu)化苦掘,