天天看點

Java高并發實戰,鎖的優化鎖優化

鎖優化

這裡的鎖優化主要是指 JVM 對 synchronized 的優化。

自旋鎖

互斥同步進入阻塞狀态的開銷都很大,應該盡量避免。在許多應用中,共享資料的鎖定狀态隻會持續很短的一段時間。自旋鎖的思想是讓一個線程在請求一個共享資料的鎖時執行忙循環(自旋)一段時間,如果在這段時間内能獲得鎖,就可以避免進入阻塞狀态。

自旋鎖雖然能避免進入阻塞狀态進而減少開銷,但是它需要進行忙循環操作占用 CPU 時間,它隻适用于共享資料的鎖定狀态很短的場景。

在 JDK 1.6 中引入了自适應的自旋鎖。自适應意味着自旋的次數不再固定了,而是由前一次在同一個鎖上的自旋次數及鎖的擁有者的狀态來決定。

鎖消除

鎖消除是指對于被檢測出不可能存在競争的共享資料的鎖進行消除。

鎖消除主要是通過逃逸分析來支援,如果堆上的共享資料不可能逃逸出去被其它線程通路到,那麼就可以把它們當成私有資料對待,也就可以将它們的鎖進行消除。

對于一些看起來沒有加鎖的代碼,其實隐式的加了很多鎖。例如下面的字元串拼接代碼就隐式加了鎖:

public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}           

複制

String 是一個不可變的類,編譯器會對 String 的拼接自動優化。在 JDK 1.5 之前,會轉化為 StringBuffer 對象的連續 append() 操作:

public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}           

複制

每個 append() 方法中都有一個同步塊。虛拟機觀察變量 sb,很快就會發現它的動态作用域被限制在 concatString() 方法内部。也就是說,sb 的所有引用永遠不會逃逸到 concatString() 方法之外,其他線程無法通路到它,是以可以進行消除。

鎖粗化

如果一系列的連續操作都對同一個對象反複加鎖和解鎖,頻繁的加鎖操作就會導緻性能損耗。

上一節的示例代碼中連續的 append() 方法就屬于這類情況。如果虛拟機探測到由這樣的一串零碎的操作都對同一個對象加鎖,将會把加鎖的範圍擴充(粗化)到整個操作序列的外部。對于上一節的示例代碼就是擴充到第一個 append() 操作之前直至最後一個 append() 操作之後,這樣隻需要加鎖一次就可以了。

輕量級鎖

JDK 1.6 引入了偏向鎖和輕量級鎖,進而讓鎖擁有了四個狀态:無鎖狀态(unlocked)、偏向鎖狀态(biasble)、輕量級鎖狀态(lightweight locked)和重量級鎖狀态(inflated)。

重量級鎖也就是通常說synchronized的對象鎖。

以下是 HotSpot 虛拟機對象頭的記憶體布局,這些資料被稱為 Mark Word。其中 tag bits 對應了五個狀态,這些狀态在右側的 state 表格中給出。除了 marked for gc 狀态,其它四個狀态已經在前面介紹過了。

Java高并發實戰,鎖的優化鎖優化

下圖左側是一個線程的虛拟機棧,其中有一部分稱為 Lock Record 的區域,這是在輕量級鎖運作過程建立的,用于存放鎖對象的 Mark Word。而右側就是一個鎖對象,包含了 Mark Word 和其它資訊。

Java高并發實戰,鎖的優化鎖優化

輕量級鎖是相對于傳統的重量級鎖而言,它使用 CAS 操作來避免重量級鎖使用互斥量的開銷。對于絕大部分的鎖,在整個同步周期内都是不存在競争的,是以也就不需要都使用互斥量進行同步,可以先采用 CAS 操作進行同步,如果 CAS 失敗了再改用互斥量進行同步。

當嘗試擷取一個鎖對象時,如果鎖對象标記為 0 01,說明鎖對象的鎖未鎖定(unlocked)狀态。此時虛拟機在目前線程的虛拟機棧中建立 Lock Record,然後使用 CAS 操作将對象的 Mark Word 更新為 Lock Record 指針。如果 CAS 操作成功了,那麼線程就擷取了該對象上的鎖,并且對象的 Mark Word 的鎖标記變為 00,表示該對象處于輕量級鎖狀态。

Java高并發實戰,鎖的優化鎖優化

如果 CAS 操作失敗了,虛拟機首先會檢查對象的 Mark Word 是否指向目前線程的虛拟機棧,如果是的話說明目前線程已經擁有了這個鎖對象,那就可以直接進入同步塊繼續執行,否則說明這個鎖對象已經被其他線程線程搶占了。如果有兩條以上的線程争用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖。

偏向鎖

偏向鎖的思想是偏向于第一個擷取鎖對象的線程,這個線程在之後擷取該鎖就不再需要進行同步操作,甚至連 CAS 操作也不再需要。

當鎖對象第一次被線程獲得的時候,進入偏向狀态,标記為 1 01。同時使用 CAS 操作将線程 ID 記錄到 Mark Word 中,如果 CAS 操作成功,這個線程以後每次進入這個鎖相關的同步塊就不需要再進行任何同步操作。

當有另外一個線程去嘗試擷取這個鎖對象時,偏向狀态就宣告結束,此時撤銷偏向(Revoke Bias)後恢複到未鎖定狀态或者輕量級鎖狀态。

Java高并發實戰,鎖的優化鎖優化