溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么用Java實現(xiàn)synchronized鎖同步機制

發(fā)布時間:2021-11-04 13:41:44 來源:億速云 閱讀:143 作者:柒染 欄目:開發(fā)技術(shù)

這期內(nèi)容當(dāng)中小編將會給大家?guī)碛嘘P(guān)怎么用Java實現(xiàn)synchronized鎖同步機制,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

synchronized 實現(xiàn)原理

synchronized 是通過進入和退出 Monitor 對象實現(xiàn)鎖機制,代碼塊通過一對 monitorenter/monitorexit 指令實現(xiàn)。在編譯后,monitorenter 指令插入到同步代碼塊的開始位置,monitorexit 指令插入到方法結(jié)束和異常處,JVM 要保證 monitorenter 和 monitorexit 成對出現(xiàn)。任何對象都有一個 Monitor 與之關(guān)聯(lián),當(dāng)且僅當(dāng)一個 Monitor 被持有后,它將處于鎖狀態(tài)。

在執(zhí)行 monitorenter 時,首先嘗試獲取對象的鎖,如果對象沒有被鎖定或者當(dāng)前線程持有鎖,鎖的計數(shù)器加 1;相應(yīng)的,在執(zhí)行 monitorexit 指令時,將鎖的計數(shù)器減 1。當(dāng)計數(shù)器減到 0 時,鎖釋放。如果在 monitorenter 獲取鎖失敗,當(dāng)前線程會被阻塞,直到對象鎖被釋放。

在 JDK6 之前,Monitor 的實現(xiàn)是依靠操作系統(tǒng)內(nèi)部的互斥鎖實現(xiàn)(一般使用的是 Mutex Lock 實現(xiàn)),線程阻塞會進行用戶態(tài)和內(nèi)核態(tài)的切換,所以同步操作是一個無差別的重量級鎖。

后來,JDK 對 synchronized 進行升級,為了避免線程阻塞時在用戶態(tài)與內(nèi)核態(tài)之間切換線程,會在操作系統(tǒng)阻塞線程前,加入自旋操作。然后還實現(xiàn) 3 種不同的 Monitor:偏向鎖(Biased Locking)、輕量級鎖(Lightweight Locking)、重量級鎖。在 JDK6 之后,synchronized 的性能得到很大的提升,相比于 ReentrantLock 而言,性能并不差,只不過 ReentrantLock 使用起來更加靈活。

適應(yīng)性自旋(Adaptive Spinning)

synchronized 對性能影響最大的是阻塞的實現(xiàn),掛起線程和恢復(fù)線程都需要操作系統(tǒng)幫助完成,需要從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài),狀態(tài)轉(zhuǎn)換需要耗費很多 CPU 時間。

在我們大多數(shù)的應(yīng)用中,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間,為了這段時間掛起和回復(fù)線程消耗的時間不值得。而且,現(xiàn)在大多數(shù)的處理器都是多核處理器,如果讓后一個線程再等一會,不釋放 CPU,等前一個釋放鎖,后一個線程立馬獲取鎖執(zhí)行任務(wù)就行。這就是所謂的自旋,讓線程執(zhí)行一個忙循環(huán),自己在原地轉(zhuǎn)一會,每轉(zhuǎn)一圈看看鎖釋放沒有,釋放了直接獲取鎖,沒有釋放就再轉(zhuǎn)一圈。

自旋鎖是在 JDK 1.4.2 引入(使用-XX:+UseSpinning參數(shù)打開),JDK 1.6 默認(rèn)打開。自旋鎖不能代替阻塞,因為自旋等待雖然避免了線程切換的開銷,但是它要占用 CPU 時間,如果鎖占用時間短,自旋等待效果挺好,反之,則是性能浪費。所以在 JDK 1.6 中引入了自適應(yīng)自旋鎖:如果同一個鎖對象,自旋等待剛成功,且持有鎖的線程正在運行,那本次自旋很有可能成功,會允許自旋等待持續(xù)時間長一些。反之,如果對于某個鎖,自旋很少成功,那之后很有可能直接省略自旋過程,避免浪費 CPU 資源。

鎖升級

Java 對象頭

synchronized 用的鎖存在于 Java 對象頭里,對象頭里的 Mark Word 里存儲的數(shù)據(jù)會隨標(biāo)志位的變化而變化,變化如下:

怎么用Java實現(xiàn)synchronized鎖同步機制

Java 對象頭 Mark Word

偏向鎖(Biased Locking)

大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低,引入偏向鎖。
當(dāng)一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程 ID,以后該線程在進入和退出同步塊時不需要進行 CAS 操作來加鎖和解鎖,只需簡單地測試一下對象頭的 Mark Word 里是否存儲著指向當(dāng)前線程的偏向鎖。引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑,因為輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時候依賴一次 CAS 原子指令(由于一旦出現(xiàn)多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小于節(jié)省下來的 CAS 原子指令的性能消耗)。

偏向鎖獲取
  • 當(dāng)鎖對象第一次被線程獲取時,對象頭的標(biāo)志位設(shè)為 01,偏向模式設(shè)為 1,表示進入偏向模式。

  • 測試線程 ID 是否指向當(dāng)前線程,如果是,執(zhí)行同步代碼塊,如果否,進入 3

  • 使用 CAS 操作把獲得到的這個鎖的線程 ID 記錄在對象的 Mark Word 中。如果成功,執(zhí)行同步代碼塊,如果失敗,說明存在過其他線程持有鎖對象的偏向鎖,開始嘗試當(dāng)前線程獲取偏向鎖

  • 當(dāng)?shù)竭_(dá)全局安全點時(沒有字節(jié)碼正在執(zhí)行),會暫停擁有偏向鎖的線程,檢查線程狀態(tài)。如果線程已經(jīng)結(jié)束,則將對象頭設(shè)置成無鎖狀態(tài)(標(biāo)志位為“01”),然后重新偏向新的線程;如果線程仍然活著,撤銷偏向鎖后升級到輕量級鎖狀態(tài)(標(biāo)志位為“00”),此時輕量級鎖由原持有偏向鎖的線程持有,繼續(xù)執(zhí)行其同步代碼,而正在競爭的線程會進入自旋等待獲得該輕量級鎖。

偏向鎖釋放

偏向鎖的釋放采用的是惰性釋放機制:只有等到競爭出現(xiàn),才釋放偏向鎖。釋放過程就是上面說的第 4 步,這里不再贅述。

關(guān)閉偏向鎖

偏斜鎖并不適合所有應(yīng)用場景,撤銷操作(revoke)是比較重的行為,只有當(dāng)存在較多不會真正競爭的同步塊時,才能體現(xiàn)出明顯改善。實踐中對于偏斜鎖的一直是有爭議的,有人甚至認(rèn)為,當(dāng)你需要大量使用并發(fā)類庫時,往往意味著你不需要偏斜鎖。

所以如果你確定應(yīng)用程序里的鎖通常情況下處于競爭狀態(tài),可以通過 JVM 參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會進入輕量級鎖狀態(tài)。

輕量級鎖(Lightweight Locking)

輕量級鎖不是用來代替重量級鎖的,它的初衷是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能損耗。

輕量級鎖獲取

如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的 Mark Word 的拷貝,官方稱之為 Displaced Mark Word。這時候線程堆棧與對象頭的狀態(tài)如下圖所示:

怎么用Java實現(xiàn)synchronized鎖同步機制

拷貝對象頭中的 Mark Word 復(fù)制到鎖記錄(Lock Record)中。

拷貝成功后,虛擬機將使用 CAS 操作嘗試將對象的 Mark Word 更新為指向 Lock Record 的指針,并將 Lock record 里的 owner 指針指向 object mark word。

如果成功,當(dāng)前線程持有該對象鎖,將對象頭的 Mark Word 鎖標(biāo)志位設(shè)置為“00”,表示對象處于輕量級鎖定狀態(tài),執(zhí)行同步代碼塊。這時候線程堆棧與對象頭的狀態(tài)如下圖所示:

怎么用Java實現(xiàn)synchronized鎖同步機制

如果更新失敗,檢查對象頭的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是,說明當(dāng)前線程擁有鎖,直接執(zhí)行同步代碼塊。

如果否,說明多個線程競爭鎖,如果當(dāng)前只有一個等待線程,通過自旋嘗試獲取鎖。當(dāng)自旋超過一定次數(shù),或又來一個線程競爭鎖,輕量級鎖膨脹為重量級鎖。重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止 CPU 空轉(zhuǎn),鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,Mark Word 中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態(tài)。

輕量級鎖解鎖
  • 輕量級鎖解鎖的時機是,當(dāng)前線程同步塊執(zhí)行完畢。

  • 通過 CAS 操作嘗試把線程中復(fù)制的 Displaced Mark Word 對象替換當(dāng)前的 Mark Word。

  • 如果成功,整個同步過程完成

  • 如果失敗,說明存在競爭,且鎖膨脹為重量級鎖。釋放鎖的同時,會喚醒被掛起的線程。

重量級鎖

輕量級鎖適應(yīng)的場景是線程近乎交替執(zhí)行同步塊的情況,如果存在同一時間訪問相同鎖對象時(第一個線程持有鎖,第二個線程自旋超過一定次數(shù)),輕量級鎖會膨脹為重量級鎖,Mark Word 的鎖標(biāo)記位更新為 10,Mark Word 指向互斥量(重量級鎖)。

重量級鎖是通過對象內(nèi)部的一個叫做監(jiān)視器鎖(monitor)來實現(xiàn)的,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)。操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,這就是為什么 JDK 1.6 之前,synchronized 重量級鎖效率低的原因。

下圖是偏向鎖、輕量級鎖、重量級鎖之間轉(zhuǎn)換對象頭 Mark Word 數(shù)據(jù)轉(zhuǎn)變:

怎么用Java實現(xiàn)synchronized鎖同步機制

偏向鎖、輕量級鎖、重量級鎖之間轉(zhuǎn)換

網(wǎng)上有一個比較全的鎖升級過程:

怎么用Java實現(xiàn)synchronized鎖同步機制

鎖升級過程

鎖消除(Lock Elimination)

鎖消除說的是虛擬機即時編譯器在運行過程中,對于一些同步代碼,如果檢測到不可能存在共享數(shù)據(jù)競爭情況,就會刪除鎖。也就是說,即時編譯器根據(jù)情況刪除不必要的加鎖操作。
鎖消除的依據(jù)是逃逸分析。簡單地說,逃逸分析就是分析對象的動態(tài)作用域。分三種情況:

  • 不逃逸:對象的作用域只在本線程本方法

  • 方法逃逸:對象在方法內(nèi)定義后,被外部方法所引用

  • 線程逃逸:對象在方法內(nèi)定義后,被外部線程所引用

即時編譯器會針對對象的不同情況進行優(yōu)化處理:

  • 對象棧上分配(Stack Allocations,HotSpot 不支持):直接在棧上創(chuàng)建對象。

  • 標(biāo)量替換(Scalar Replacement):將對象拆散,直接創(chuàng)建被方法使用的成員變量。前提是對象不會逃逸出方法范圍。

  • 同步消除(Synchronization Elimination):就是鎖消除,前提是對象不會逃逸出線程。

對于鎖消除來說,就是逃逸分析中,那些不會逃出線程的加鎖對象,就可以直接刪除同步鎖。

通過代碼看一個例子:

public void elimination1() {
    final Object lock = new Object();
    synchronized (lock) {
        System.out.println("lock 對象沒有只會作用域本線程,所以會鎖消除。");
    }
}

public String elimination2() {
    final StringBuffer sb = new StringBuffer();
    sb.append("Hello, ").append("World!");
    return sb.toString();
}

public StringBuffer notElimination() {
    final StringBuffer sb = new StringBuffer();
    sb.append("Hello, ").append("World!");
    return sb;
}

elimination1()中的鎖對象lock作用域只是方法內(nèi),沒有逃逸出線程,elimination2()中的sb也就這樣,所以這兩個方法的同步鎖都會被消除。但是notElimination()方法中的sb是方法返回值,可能會被其他方法修改或者其他線程修改,所以,單看這個方法,不會消除鎖,還得看調(diào)用方法。

鎖粗化(Lock Coarsening)

原則上,我們在編寫代碼的時候,要將同步塊作用域的作用范圍限制的盡量小。使得需要同步的操作數(shù)量盡量少,當(dāng)存在鎖競爭時,等待線程盡快獲取鎖。但是有時候,如果一系列的連續(xù)操作都對同一個對象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有出現(xiàn)線程競爭,頻繁地進行互斥同步操作也會導(dǎo)致不必要的性能損耗。如果虛擬機檢測到有一串零碎的操作都是對同一對象的加鎖,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部。
比如上面例子中的elimination2()方法中,StringBuffer的append是同步方法,頻繁操作時,會進行鎖粗化,最后結(jié)果會類似于(只是類似,不是真實情況):

public String elimination2() {
    final StringBuilder sb = new StringBuilder();
    synchronized (sb) {
        sb.append("Hello, ").append("World!");
        return sb.toString();
    }
}

或者

public synchronized String elimination3() {
    final StringBuilder sb = new StringBuilder();
    sb.append("Hello, ").append("World!");
    return sb.toString();
}

文末總結(jié)

  • 同步操作中影響性能的有兩點:

    • 加鎖解鎖過程需要額外操作

    • 用戶態(tài)與內(nèi)核態(tài)之間轉(zhuǎn)換代價比較大

  • synchronized 在 JDK 1.6 中有大量優(yōu)化:分級鎖(偏向鎖、輕量級鎖、重量級鎖)、鎖消除、鎖粗化等。

  • synchronized 復(fù)用了對象頭的 Mark Word 狀態(tài)位,實現(xiàn)不同等級的鎖實現(xiàn)。

上述就是小編為大家分享的怎么用Java實現(xiàn)synchronized鎖同步機制了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI