為什么需要偏向鎖
當(dāng)多個(gè)處理器同時(shí)處理的時(shí)候忘蟹,通常需要處理互斥的問題。
一般的解決方式都會(huì)包含acquire和release這個(gè)兩種操作,操作保證滞伟,一個(gè)線程在acquire執(zhí)行之后,在它執(zhí)行release之前炕贵,其它線程不能完成acquire操作梆奈。這個(gè)過程經(jīng)常就涉及到鎖。研究表明(L. Lamport A fast mutual execlusion algorithm)称开,通過 fast locks算法可以做到亩钟,lock和unlock操作所需的時(shí)間與潛在的競(jìng)爭(zhēng)處理器數(shù)無關(guān)。
java內(nèi)置了monitor來處理多線程競(jìng)爭(zhēng)的情況.
一種優(yōu)化方式是使用 輕量鎖來在大多數(shù)情況下避免重量鎖的使用鳖轰,輕量鎖的主要機(jī)制是在monitor entry的時(shí)候使用原子操作清酥,某些退出操作也是這樣,如果有競(jìng)爭(zhēng)發(fā)生就轉(zhuǎn)而退避到使用操作系統(tǒng)的互斥量
輕量鎖認(rèn)為大多數(shù)情況下都不會(huì)產(chǎn)生競(jìng)爭(zhēng)
在鎖的使用中一般會(huì)使用幾種原子指令:
CAS:檢查給定指針位置的值和傳入的值是否一致蕴侣,如果一致焰轻,就修改
SWAP:替換指針原位置的值,并返回舊的值
membar:內(nèi)存屏障約束了處理器在處理指令時(shí)的重排序情況昆雀,比如禁止同讀操作被重排序到寫操作之后
Java中使用 two-word 對(duì)象頭
是 mark word,它包括同步信息辱志,垃圾回收信息、hash code信息
指向?qū)ο蟮闹羔槍?duì)象
這些指令的花銷很昂貴狞膘,因?yàn)樗麄兊膶?shí)現(xiàn)通常會(huì)耗盡處理器的重排序緩沖區(qū)揩懒,從而限制了處理器原本能夠像流水線一樣處理指令的能力。研究數(shù)據(jù)發(fā)現(xiàn)(Eliminating_synchronization-related_atomic_operations_with_biased_locking_and_bulk_rebiasing)原子操作在真實(shí)的應(yīng)用中客冈,比如javac 旭从,會(huì)導(dǎo)致性能下降20%。
另一種優(yōu)化的方式是使用偏向鎖,它不僅認(rèn)為大多數(shù)情況下是沒有競(jìng)爭(zhēng)的和悦,而且在整個(gè)的monitor的一生中退疫,都只會(huì)有一個(gè)線程來執(zhí)行enter和exit,這樣的監(jiān)視器就很適合偏向于這個(gè)線程了鸽素。當(dāng)然如果這時(shí)有另外一個(gè)線程嘗試進(jìn)入偏向鎖褒繁,即使沒有發(fā)生競(jìng)爭(zhēng),也需要執(zhí)行 偏向鎖撤銷操作
輕量鎖
當(dāng)輕量鎖通過monitorenter指令獲取鎖的時(shí)候馍忽,鎖記錄肯定會(huì)被記錄到線程的棧里面去棒坏,以表示鎖獲取操作。鎖記錄會(huì)持有原始對(duì)象的mark word和一些必備的元數(shù)據(jù)來識(shí)別鎖住的對(duì)象遭笋。在獲取鎖的時(shí)候坝冕,mark word會(huì)被拷貝一份到鎖記錄(這個(gè)操作稱為 displaced mark word)然后執(zhí)行CAS操作嘗試是的對(duì)象的mark word指針指向鎖記錄。如果CAS成功瓦呼,當(dāng)前線程就持有了鎖喂窟,如果失敗,其它線程獲取鎖,這是鎖就“膨脹”央串,轉(zhuǎn)而使用了操作系統(tǒng)的互斥量和條件磨澡,在“膨脹”的過程中,對(duì)象本身的mark word會(huì)經(jīng)過CAS操作指向含有mutex和condition的數(shù)據(jù)結(jié)構(gòu)质和。
當(dāng)執(zhí)行unlock的時(shí)候稳摄,扔通過CAS來操作mark word,如果CAS成功了,說明沒有競(jìng)爭(zhēng)饲宿,同時(shí)維持輕量鎖厦酬;如果失敗了,鎖就處于競(jìng)爭(zhēng)態(tài)褒傅,當(dāng)被持有時(shí)弃锐,會(huì)以一種“非常慢”的方式來正確的釋放鎖并通知其他等待線程來獲取鎖
同一個(gè)線程重新處理的方式很直白袄友,在輕量鎖發(fā)現(xiàn)要獲取的鎖已經(jīng)被當(dāng)前線程持有的時(shí)候殿托,它會(huì)存一個(gè)0進(jìn)去,而不對(duì)mark word做任何處理剧蚣,同樣在unlock的時(shí)候支竹,如果有看到0,也不會(huì)更新對(duì)象的mark word.并每次重入鸠按,都會(huì)明確的記錄count礼搁。
偏向鎖的實(shí)現(xiàn)
線程指針是NULL(0)表示當(dāng)前沒有線程被偏向這個(gè)對(duì)象
當(dāng)分配一個(gè)對(duì)象并且這個(gè)對(duì)象能夠執(zhí)行偏向的時(shí)候并且還沒有偏向時(shí),會(huì)執(zhí)行CAS是的當(dāng)前線程ID放入到mark word的線程ID區(qū)域目尖。
如果成功馒吴,對(duì)象本身就會(huì)被偏向到當(dāng)前線程,當(dāng)前線程會(huì)成為偏向所有者
> 線程ID直接指向JVM內(nèi)部表示的線程;java虛擬機(jī)中則是在最后3bit填充0x5表示偏向模式。
如果CAS失敗了饮戳,即另一個(gè)線程已經(jīng)成為偏向的所有者豪治,這意味著這個(gè)線程的偏向必須撤銷。對(duì)象的狀態(tài)會(huì)變成輕量鎖的模式扯罐,為了達(dá)到這一點(diǎn)负拟,嘗試把對(duì)象偏向于自己的線程必須能夠操作偏向所有者的棧,為此需要全局安全點(diǎn)已經(jīng)觸達(dá)(沒有線程在執(zhí)行字節(jié)碼)歹河。此時(shí)偏向擁有者會(huì)像輕量級(jí)鎖操作那樣掩浙,它的堆棧會(huì)填入鎖記錄,然后對(duì)象本身的mark word會(huì)被更新成指向棧上最老的鎖記錄秸歧,然后線程本身在安全點(diǎn)的阻塞會(huì)被釋放
> 如果沒有被原有的偏向鎖持有者持有厨姚,會(huì)撤銷對(duì)象重新回到可偏向但是還沒有偏向的狀態(tài),然后嘗試重新獲取鎖键菱。如果對(duì)象當(dāng)前鎖住了是進(jìn)入輕量鎖遣蚀,如果沒有鎖住是進(jìn)入未被鎖定的,不可偏向?qū)ο?/p>
下一個(gè)獲取鎖的操作會(huì)與檢測(cè)對(duì)象的mark word,如果對(duì)象是可偏向的纱耻,并且偏向的所有者是當(dāng)前那線程芭梯,會(huì)沒有任何額外操作而立馬獲取鎖。
這個(gè)時(shí)候偏向鎖的持有者的棧不會(huì)初始化鎖記錄弄喘,因?yàn)閷?duì)象偏向的時(shí)候玖喘,是永遠(yuǎn)不會(huì)檢驗(yàn)鎖記錄的
unlock的時(shí)候,會(huì)測(cè)試mark word的狀態(tài)蘑志,看是否仍然有偏向模式累奈。如果有,就不會(huì)再做其它的測(cè)試急但,甚至不需要管線程ID是不是當(dāng)前線程ID
這里通過解釋器的保證monitorexit操作只會(huì)在當(dāng)前線程執(zhí)行澎媒,所以這也是一個(gè)不需要檢查的理由
不適用偏向鎖的模式
生產(chǎn)生-消費(fèi)者模式,會(huì)有過個(gè)線程參與競(jìng)爭(zhēng)波桩;
一個(gè)線程分配多個(gè)對(duì)象戒努,然后給每個(gè)對(duì)象執(zhí)行初始的同步操作,再有其它線程來處理子流程
批量回到可偏向狀態(tài)還是撤銷可偏向镐躲?
經(jīng)驗(yàn)發(fā)現(xiàn)為特定的數(shù)據(jù)結(jié)構(gòu)選擇性的禁用偏向鎖(Store-fremm biased lock SFBL)來避免不合適的情況是合理的储玫。為此需要考慮每個(gè)數(shù)據(jù)結(jié)構(gòu)到底是執(zhí)行撤銷偏向的消耗小還是重新回到可偏向的狀態(tài)消耗下。一種啟發(fā)式的方式來決定到底是執(zhí)行那種方式萤皂,在每個(gè)類的元數(shù)據(jù)里面都會(huì)包含一個(gè)counter和時(shí)間戳撒穷,每次偏向鎖的實(shí)例執(zhí)行一次偏向撤銷,都會(huì)自增裆熙,時(shí)間戳用于記錄上次執(zhí)行bulk rebias的時(shí)間端礼。
撤銷計(jì)數(shù)并統(tǒng)計(jì)那些處于可偏向但是未偏向狀態(tài)的撤銷禽笑,這些操作的撤銷只需要一次CAS就可以
counter本身有兩個(gè)閾值,一個(gè)是bulk rebias閾值蛤奥,一個(gè)是bulk revocation蒲每。剛開始的時(shí)候,這種啟發(fā)式的算法可以單獨(dú)的決定執(zhí)行rebias還是revoke,一單bulk rebias的閾值達(dá)到喻括,就會(huì)執(zhí)行bulk rebias邀杏,轉(zhuǎn)移到 rebiasable狀態(tài)
time閾值用來重置撤銷的計(jì)數(shù)counter,如果自從上次執(zhí)行bulk bias已經(jīng)超過了這個(gè)閾值時(shí)間,就會(huì)發(fā)生counter的重置唬血。
這意味著從上次執(zhí)行bulk rebias到現(xiàn)在并沒有執(zhí)行多次的撤銷操作望蜡,也就是說執(zhí)行bias仍然是個(gè)不錯(cuò)的選擇
但是如果在執(zhí)行了bulk rebias之后,在時(shí)間閾值之內(nèi)拷恨,仍然一直有撤銷數(shù)量增長(zhǎng)脖律,一旦達(dá)到了bulk revocation的閾值,就會(huì)執(zhí)行bulk revocation,此時(shí)這個(gè)類的對(duì)象不會(huì)再被允許使用偏向鎖腕侄。
Hotspot中的閾值如下 Bulk rebias threshold 20 Bulk revoke threshold 40 Decay time 25s
撤銷偏向本身是一個(gè)消耗很大的事情小泉,因?yàn)樗仨殥炱鹁€程,遍歷棧找到并修改lock records(鎖記錄)
最明顯的查找某個(gè)數(shù)據(jù)結(jié)構(gòu)的所有對(duì)象實(shí)例的方式就是遍歷堆冕杠,這種方式在堆比較小的時(shí)候還可以微姊,但是堆變大就顯得性能不好。為類解決這個(gè)為題分预,使用epoch兢交。epoch是一個(gè)時(shí)間戳,用來表明偏向的合法性笼痹,只要這個(gè)數(shù)據(jù)接口是可偏向的配喳,那么就會(huì)在mark word上有一個(gè)對(duì)應(yīng)的epoch bit位
這個(gè)時(shí)候,一個(gè)對(duì)象被認(rèn)為已經(jīng)偏向了線程T必須滿足兩個(gè)條件凳干,1: mark word中偏向所有這的標(biāo)記必須是這個(gè)線程晴裹,2:實(shí)例的epoch必須是和數(shù)據(jù)結(jié)構(gòu)的epoch相等
epoch本身的大小是限制的,也就是有可能出現(xiàn)循環(huán)救赐,但這并不影響方案的正確性
通過這種方式涧团,類C的bulk rebiasing操作會(huì)少去很多的花銷。具體操作如下
增大類C的epoch净响,它本身是一個(gè)固定長(zhǎng)度的integer,和對(duì)象頭中的epoch擁有一樣的bit位數(shù)
掃描所有的線程棧來定位當(dāng)前類C的實(shí)例中已經(jīng)鎖住的少欺,更新他們的epoch為類C的新的epoch或者是,根據(jù)啟發(fā)式策略撤銷偏向
這樣就不用掃描堆了馋贤,對(duì)于那些沒有被改變epoch的實(shí)例(和類的epoch不同),會(huì)被自動(dòng)當(dāng)做可偏向但是還沒有偏向的狀態(tài)
這種狀態(tài)可看做 rebiaseable
當(dāng)前HotSpot虛擬機(jī)的實(shí)現(xiàn)
批量撤銷本身存在著性能問題畏陕,一般的解決方式如下
添加epoch,如前所訴
線程第一次獲取的時(shí)候不偏向配乓,而是在執(zhí)行一定數(shù)量后都有同一個(gè)線程獲取再偏向
允許鎖具有永遠(yuǎn)改變(或者很少)的固定偏向線程,并且允許非偏向線程獲取鎖而不是撤銷鎖。
這種方式必須確保獲取鎖的線程必須確保進(jìn)去臨界區(qū)之前沒有其它線程持有鎖犹芹,并且不能使用 read-modify-write的指令崎页,只能使用read和write
當(dāng)前Hotspot JVM中的在32位和64位有不同的形式
64bit為
32bit為
輕量鎖(thin locks),細(xì)節(jié)如前所述。它在HotSpot中使用displaced header的方式實(shí)現(xiàn)腰埂,又被稱作棧鎖
mark完整的狀態(tài)轉(zhuǎn)換關(guān)系如下
剛分配對(duì)象飒焦,此時(shí)對(duì)象是可偏向并且未偏向的
對(duì)象偏向于線程T,并記下epoch
此時(shí)有新線程來競(jìng)爭(zhēng)
3.1一種策略是T執(zhí)行對(duì)應(yīng)的unlock屿笼,并重新分配給新的線程,以便不需要執(zhí)行撤銷操作
3.2 如果已經(jīng)偏向的對(duì)象被其它線程通過wait或者notify操作了牺荠,里面進(jìn)入膨脹裝態(tài),使用重量鎖
此時(shí)有新的線程來競(jìng)爭(zhēng)驴一,一種策略是使用啟發(fā)式的方式來統(tǒng)計(jì)撤銷的次數(shù)
4.1 當(dāng)撤銷達(dá)到bulk rebias的閾值時(shí)休雌,執(zhí)行bulk rebias
4.2 當(dāng)撤銷達(dá)到bulk revoke,并且此時(shí)所仍然被持有(原偏向鎖持有者)肝断,轉(zhuǎn)向輕量鎖(hashcode的計(jì)算依賴于膨脹來支持修改displaced mark word)
4.3 當(dāng)撤銷達(dá)到bulk revoke杈曲,并且此時(shí)所沒有被持有(原偏向鎖持有者),轉(zhuǎn)向未被鎖定不可偏向的狀態(tài)胸懈,此時(shí)沒有進(jìn)行hashcode計(jì)算
對(duì)于經(jīng)過bulk rebias的對(duì)象担扑,檢查期間沒有鎖定的實(shí)例,它的epoch會(huì)和class的不一樣趣钱,變成過期魁亦,但是可以偏向
5.1 如果 發(fā)生垃圾回收,lock會(huì)被初始化成可偏向但未偏向的狀態(tài)(這也可以降低epoch循環(huán)使用的影響)
5.2 如果重新被線程獲取偏向鎖羔挡,回到偏向鎖獲取狀態(tài)
處于輕量鎖狀態(tài)洁奈,它可能沒有hashcode計(jì)算,可能有绞灼,這依賴于inflat
6.1 沒有hashcode利术,此時(shí)解鎖回到?jīng)]有hashcode計(jì)算的不可偏向的狀態(tài)
6.2 又被其它線程占有,轉(zhuǎn)移到重量鎖(比如使用POXIS操作系統(tǒng)的mutex和condition)
未被鎖定不可偏向的狀態(tài)同時(shí)沒有hashcode計(jì)算加鎖后轉(zhuǎn)移到輕量鎖
處于重量鎖狀態(tài)
8.1? 8.2? 如果在Stop-The-Word期間沒有競(jìng)爭(zhēng)了低矮,就可以去膨脹(STW期間沒有其它線程獲取和釋放鎖印叁,是安全的),根據(jù)是否有hashcode,退到對(duì)應(yīng)的狀態(tài)(就是就退回使用偏向鎖 )
8.3 重量鎖期間的lock/unlock仍然處于重量鎖
計(jì)算過hashcode军掂,再加鎖和解鎖對(duì)應(yīng)狀態(tài)轉(zhuǎn)換(9.10)
在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群轮蜕。交流學(xué)習(xí)群號(hào):938837867 暗號(hào):555 里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis蝗锥,Netty源碼分析跃洛,高并發(fā)、高性能终议、分布式汇竭、微服務(wù)架構(gòu)的原理葱蝗,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備