Java的對(duì)象鎖

內(nèi)置鎖

Java提供了一種內(nèi)置的鎖機(jī)制來支持原子性可見性同步代碼塊(Synchronized Block)。同步代碼塊包括兩部分:一個(gè)是作為鎖的對(duì)象引用,一個(gè)是鎖保護(hù)的代碼塊。每一個(gè)Java對(duì)象都可以用做一個(gè)實(shí)現(xiàn)同步的鎖,這種鎖被稱為內(nèi)置鎖(Intrinsic Lock)或者監(jiān)視器鎖(Monitor Lock)。線程進(jìn)入同步代碼塊之前會(huì)自動(dòng)獲得鎖,并且在退出同步代碼塊時(shí)(正常返回,或者是異常退出)會(huì)自動(dòng)釋放鎖。
同步代碼塊以關(guān)鍵字synchronized修飾,例如:

synchronized(鎖對(duì)象引用){
//鎖保護(hù)的代碼塊
}

如果synchronized修飾的是對(duì)象的方法,那么被修飾的方法體就是同步代碼塊,鎖的對(duì)象引用就是被修飾的方法所在的對(duì)象。

public class SyncTest {
    public synchronized void method() {
    //方法體就是同步代碼塊
    } 
}

如果synchronized修飾的是靜態(tài)方法,那么被修飾的方法體就是同步代碼塊,鎖的對(duì)象引用就是被修飾的方法所在的Class對(duì)象。

public class SyncTest {
    public static synchronized void method() {
    //方法體就是同步代碼塊
    } 
}

內(nèi)置鎖的特性

  • 互斥:同一時(shí)間最多只有一個(gè)線程能夠持有這種鎖。
    線程嘗試獲取一個(gè)被其它線程持有的內(nèi)置鎖,線程必須等待(自旋)或者阻塞(自旋策略失效),并且因?yàn)檎?qǐng)求內(nèi)置鎖被阻塞的線程不能被中斷
  • 可重入:如果某個(gè)線程試圖獲取一個(gè)已經(jīng)由它持有的內(nèi)置鎖,那么這個(gè)請(qǐng)求就會(huì)成功。
    實(shí)現(xiàn)原理:為每個(gè)所關(guān)聯(lián)一個(gè)獲取計(jì)數(shù)值和一個(gè)所有者線程。當(dāng)計(jì)數(shù)值為0,表示這個(gè)鎖沒有被任何線程持有。當(dāng)線程請(qǐng)求一個(gè)未被持有的鎖,JVM將所有者線程設(shè)置為請(qǐng)求線程,并且將計(jì)數(shù)值置為1。如果同一個(gè)線程再次請(qǐng)求這個(gè)鎖,計(jì)數(shù)值遞增,退出同步代碼塊計(jì)數(shù)值將遞減。如果計(jì)數(shù)值為0,這個(gè)鎖將被釋放。

synchronized的原理

當(dāng)聲明 synchronized 代碼塊時(shí),編譯而成的字節(jié)碼將包含 monitorenter 和 monitorexit 指令。這兩種指令均會(huì)消耗操作數(shù)棧上的一個(gè)引用類型的元素(也就是 synchronized 關(guān)鍵字括號(hào)里的引用),作為所要加鎖和解鎖的鎖對(duì)象。

自旋

因?yàn)楸O(jiān)視器鎖實(shí)現(xiàn)的同步是互斥同步,互斥導(dǎo)致的Java 線程的阻塞以及喚醒,都是依靠操作系統(tǒng)來完成的。舉例來說,對(duì)于符合 posix 接口的操作系統(tǒng)(如 macOS 和絕大部分的 Linux),上述操作是通過 pthread 的互斥鎖(mutex)來實(shí)現(xiàn)的。這些操作將涉及系統(tǒng)調(diào)用,需要從操作系統(tǒng)的用戶態(tài)切換至內(nèi)核態(tài),這些操作給操作系統(tǒng)的并發(fā)性能帶來了很大的開銷。
自旋的實(shí)現(xiàn)原理就是,如果線程請(qǐng)求獲取監(jiān)視器鎖失敗,并不立刻阻塞線程,而是讓線程執(zhí)行一個(gè)忙循環(huán)(自旋)。自旋之后再次嘗試獲取鎖。如果獲取鎖失敗,這個(gè)過程會(huì)循環(huán)一定次數(shù),超過某個(gè)閥值,如果還是獲取不到鎖,才阻塞線程。自旋可以通過-XX:+UserSpinning參數(shù)來開啟,自旋的次數(shù)通過-XX:PreBlockSpin來更改(默認(rèn)是10)。
自旋雖然避免了線程切換的損耗,但是需要占用處理器時(shí)間。自旋的效果取決于鎖被占用的時(shí)間,如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)很好,反之,自旋只會(huì)白白消耗處理器資源,帶來性能上的損耗。
JDK1.6引入了自適應(yīng)的自旋鎖。

鎖優(yōu)化

自旋鎖

見自旋小節(jié)

重量級(jí)鎖

重量級(jí)鎖是 JVM中傳統(tǒng)的鎖實(shí)現(xiàn)。在這種狀態(tài)下,Java 虛擬機(jī)會(huì)阻塞加鎖失敗的線程,并且在目標(biāo)鎖被釋放的時(shí)候,喚醒這些線程。

輕量級(jí)鎖

輕量級(jí)鎖的目標(biāo)是在沒有多線程的競爭下,減少重量級(jí)鎖使用的操作系統(tǒng)互斥量產(chǎn)生的性能消耗。

原理

輕量級(jí)鎖的實(shí)現(xiàn)依賴對(duì)象頭的標(biāo)記字段。Java的對(duì)象頭被設(shè)計(jì)為能夠根據(jù)對(duì)象的狀態(tài)復(fù)用自己的儲(chǔ)存空間,對(duì)象頭的標(biāo)記字段有2bit用于儲(chǔ)存鎖狀態(tài),不同的鎖狀態(tài)對(duì)應(yīng)的對(duì)象頭的內(nèi)容及狀態(tài)之間轉(zhuǎn)換如下圖:


對(duì)象頭標(biāo)記字段的鎖狀態(tài)的轉(zhuǎn)化
加鎖過程

當(dāng)進(jìn)行加鎖操作時(shí),JVM會(huì)判斷是否已經(jīng)是重量級(jí)鎖。如果不是,它會(huì)在當(dāng)前線程的當(dāng)前棧楨中劃出一塊空間,作為該鎖的鎖記錄(Lock Record),并且將鎖對(duì)象的標(biāo)記字段復(fù)制到該鎖記錄中。
然后,JVM會(huì)嘗試用 CAS(compare-and-swap)操作將鎖對(duì)象的標(biāo)記字段替換為鎖記錄的指針。如果操作成功,那么這個(gè)線程就獲取了這個(gè)對(duì)象的鎖,并且標(biāo)記字段的鎖標(biāo)志位轉(zhuǎn)變?yōu)椤?0”,表示該鎖處于輕量級(jí)鎖定狀態(tài)。
如果標(biāo)記字段替換操作失敗,JVM會(huì)先檢查對(duì)象的標(biāo)記字段是否指向當(dāng)前線程的棧幀,如果是表明當(dāng)前線程已經(jīng)持有了該對(duì)象的鎖。否則說明這個(gè)對(duì)象的鎖已經(jīng)被其它線程所持有了,這時(shí),輕量級(jí)鎖膨脹為重量級(jí)鎖,鎖的標(biāo)記字段的鎖標(biāo)志位變?yōu)椤?0”,標(biāo)記字段存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面的線程要進(jìn)入阻塞狀態(tài)。

解鎖過程

輕量級(jí)鎖的解鎖過程也要通過CAS操作來進(jìn)行。如果鎖對(duì)象的標(biāo)記字段仍然指向線程的鎖記錄,JVM嘗試用CAS操作將鎖對(duì)象的標(biāo)記字段替換為鎖記錄中的復(fù)制過來的標(biāo)記字段。如果CAS操作成功,則釋放了鎖。否則說明有其他線程嘗試獲取過該對(duì)象的鎖,那么在釋放鎖的同時(shí),喚醒被掛起的線程。

性能分析

輕量級(jí)鎖提升性能的依據(jù)是:對(duì)于絕大部分的鎖,在整個(gè)同步期間都是不存在競爭的。如果沒有競爭,輕量級(jí)鎖使用CAS操作避免了重量級(jí)鎖使用互斥量的開銷。如果存在競爭,除了重量級(jí)鎖的互斥量開銷,還帶來了CAS操作的開銷,性能反而比重量級(jí)鎖差。

偏向鎖

偏向鎖的目的也是消除無競爭條件下的同步原語。偏向鎖會(huì)偏向于第一個(gè)獲取到它的線程,如果在獲取到鎖之后的過程中,沒有發(fā)生鎖競爭,那么持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。相比輕量級(jí)鎖,偏向鎖能夠消除輕量級(jí)鎖多次加鎖的CAS操作。

加鎖過程

具體來說,在線程進(jìn)行加鎖時(shí),如果該鎖對(duì)象支持偏向鎖,那么 JVM會(huì)通過 CAS 操作,將當(dāng)前線程的ID記錄在鎖對(duì)象的標(biāo)記字段之中,并且將標(biāo)記字段的鎖標(biāo)志位置為“01”,即偏向模式。
如果操作成功,在接下來的運(yùn)行過程中,每當(dāng)有線程請(qǐng)求這把鎖,Java 虛擬機(jī)只需判斷鎖對(duì)象標(biāo)記字段中:最后三位是否為 101,是否包含當(dāng)前線程的ID,以及 epoch 值是否和鎖對(duì)象的類的 epoch 值相同。如果都滿足,那么當(dāng)前線程持有該偏向鎖,可以直接返回。
當(dāng)有另外的線程嘗試獲取這個(gè)鎖時(shí),偏向模式宣告結(jié)束。如果當(dāng)前對(duì)存于未鎖定狀態(tài),撤銷偏向恢復(fù)至未鎖定(標(biāo)記字段的鎖標(biāo)志位為“01”)。如果處于鎖定狀態(tài),則升級(jí)為輕量級(jí)鎖(標(biāo)記字段的鎖標(biāo)志位為“00”),后續(xù)的同步操作便按照輕量級(jí)鎖的規(guī)則來進(jìn)行。

偏向鎖失效

如果某一類鎖對(duì)象的總撤銷數(shù)超過了一個(gè)閾值(對(duì)應(yīng)JVM參數(shù)-XX:BiasedLockingBulkRebiasThreshold,默認(rèn)為 20),那么 JVM會(huì)宣布這個(gè)類的偏向鎖失效。
如果總撤銷數(shù)超過另一個(gè)閾值(對(duì)應(yīng) JVM參數(shù) -XX:BiasedLockingBulkRevokeThreshold,默認(rèn)值為 40),那么 Java 虛擬機(jī)會(huì)認(rèn)為這個(gè)類已經(jīng)不再適合偏向鎖。此時(shí),Java 虛擬機(jī)會(huì)撤銷該類實(shí)例的偏向鎖,并且在之后的加鎖過程中直接為該類實(shí)例設(shè)置輕量級(jí)鎖。

其它優(yōu)化手段

  • 鎖消除
    JVM通過逃逸分析,如果判斷出在一段代碼中,堆上的所有數(shù)據(jù)都不會(huì)逃逸出去從而被其它線程訪問到,就可以把他們當(dāng)做棧上數(shù)據(jù)看待,同步加鎖就不需要進(jìn)行。
  • 鎖粗化
    如果一系列的連續(xù)操作都是對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作出現(xiàn)在循環(huán)體中(如StringBuffer的連續(xù)多次append操作),JVM會(huì)將加鎖同步的范圍擴(kuò)展到這個(gè)操作系列的外部。

參考

HotSpot-Synchronization

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容