說一說 JVM 對鎖的優化

Howie_Y發表於2019-01-13

JDK 1.6 對併發性進行了很大的改進,這也是為了使執行緒之間更好更高效地共享資料,解決競爭問題,實現執行緒安全。因此從 JDK 1.6 開始,實現了很多鎖的優化技術。

一. 從 ReentrantLock 和 synchronized 看鎖的優化

講正題之前,先說一下 ReentrantLock 和 synchronized 這對冤家,我們經常會拿這兩個鎖作比較,其中一個是顯式鎖,實現於 Lock 介面;而另外一個是隱式鎖,更加的原生。

如果我們從效能上來比較的話,在 JDK 1.6 以前,多執行緒環境下的 synchronized 效能明顯差於 ReentrantLock;但在 JDK 1.6 及其之後的版本中,兩者的效能已經基本持平,而且我們通常優先考慮使用 synchronized 進行同步。究其原因,就是“鎖優化”。

二. 自旋鎖

其實自旋鎖在 JDK 1.4.2 中已經引入,不過當時的預設狀態為關閉;在 JDK 1.6 中改為預設開啟。

1. 產生的原因

在互斥同步中,阻塞對效能的影響是最大的,掛起執行緒和恢復執行緒兩個操作(即執行緒的切換)給了併發效能很大的壓力。

但是很多時候,共享資源處於鎖定狀態的時間其實非常短,為了那麼短的時間而去對執行緒反覆地掛起與恢復明顯十分不值得。因此我們可以利用自旋鎖避免這兩個操作。

2. 原理

當一個執行緒在請求一個被持有的鎖時,讓這個執行緒執行一個空迴圈(自旋),此時並不會放棄處理器的執行,如果鎖很快就被釋放,那麼就避免了對這個執行緒的掛起與恢復操作。

3. 利弊得失

自旋本身避免了執行緒切換帶來的開銷,但也佔用了處理器的時間。如果鎖被佔用的時間很短,那自旋鎖的效果自然很好;但如果時間很長,那麼這個自旋的執行緒就白白消耗了處理器的資源,反而適得其反,浪費了效能。

因此,自旋等待的時間是有限度的,一旦超過了自旋的限度次數,那麼就會使用傳統的方法進行阻塞,即掛起該執行緒。

4. 自適應自旋

JDK 1.6 中對自旋鎖進行了改進,引入了自適應自旋鎖,使得自旋的時間不再固定。簡單來說,就是隨著程式的執行和效能的監控,JVM 會對鎖的情況進行預測,從而給出適合的自旋時間,更加 “智慧”。

三. 鎖消除

JVM 會對於一些程式碼上要求同步,但被檢測到不可能存在共享資料競爭的鎖進行消除。

例子:

public void add(String str1, String str2) {
    StringBuffer sb = new StringBuffer();
    sb.append(str1).append(str2);
}
複製程式碼

眾所周知,StringBuffer 的 append 方法是同步方法,但是在這個 add 方法中,StringBuffer 不會存在共享資源競爭的情況,因為其他執行緒並不會訪問到它。這就符合了 “程式碼上要求同步,但不可能存在共享資料競爭” 的條件。因此雖然這裡有鎖,但是可以安全地清除掉,避免了鎖的獲取釋放帶來的效能消耗。

四. 鎖粗化

通常情況下,我們編寫程式碼時,都儘可能地將同步塊的作用範圍縮小,使得鎖的持有時間儘可能地縮短,提高細粒度,增加併發度,降低鎖的競爭。

但是有些情況下,如果一系列連續的操作中我們不斷地加鎖解鎖,比如在迴圈之中,那麼也會造成不必要的效能損耗。

比如:

public void add(String str1, String str2, String str3) {
    StringBuffer sb = new StringBuffer();
    sb.append(str1);
    sb.append(str2);
    sb.append(str3);
}
複製程式碼

同樣是 StringBuffer ,JVM 檢測到有一連串操作都對同一個物件(sb)加鎖時,就會把鎖進行粗化處理,擴充套件同步範圍,這樣從一個 append() 到最後一個,只需要加一次鎖就可以了。

五. 偏向鎖

1. 產生的原因

大多數情況下,鎖不僅不存在多執行緒競爭狀態,而且通常由同一個執行緒多次獲得,因此,我們有必要減少同一個執行緒多次獲得同一個鎖的效能消耗。

2. 原理

當鎖物件第一次被執行緒獲取的時候,虛擬機器在物件的物件頭中標誌為偏向模式,同時使用 CAS 操作把獲取到這個鎖的執行緒的 ID 記錄在物件頭的 Mark Word 資料中。(這部分不瞭解的讀者可以去學習一下 JVM 的“物件記憶體佈局”)

只要 CAS 操作獲取成功,該鎖物件便 “偏向” 了這個執行緒,只要不出現第二個執行緒,這個鎖物件的物件頭就會一直記錄著該執行緒的 id。

這時,獲得偏向鎖的執行緒以後每次進入這個鎖的時候都不再需要進行同步操作,一路暢通。

那如果出現了第二個執行緒會發生什麼呢?我們繼續往下看。

六. 輕量級鎖

當偏向鎖失效後,便會升級為輕量鎖

1. 原理

當一個執行緒企圖持有一個鎖的時候,倘若這個鎖已經是偏向狀態,那麼這個時候會將偏向狀態解除,然後在競爭這個鎖的執行緒的棧幀中建立一個鎖記錄的空間(Lock Record),並把鎖物件的 Mark Word 拷貝到裡面來,記作 Displaced Mark Word。

然後,JVM 再使用 CAS 操作將鎖物件的 Mark Word 更新為指向其中一個執行緒的 Lock Record 的指標,當這個操作成功,這個執行緒也就持有了該輕量鎖。

當然,輕量鎖的持有和釋放,都需要 CAS 操作進行。釋放鎖的時候,只需要把棧幀裡的 Displaced markd word 使用 CAS 複製回去即可。如果 CAS 操作獲取鎖失敗,JVM 會首先檢查一下鎖物件的 Mark Word 是否指向當前執行緒,是則可以直接通行,否則先自旋一下吧。

2. 適應情況

這個鎖適應的是沒有競爭或是隻有輕度競爭的情況,若是傳送了輕度的競爭,只需要進行幾次自旋即可。

但是一旦發生長時間的競爭,輕量級鎖就會升級為重量級鎖,這時候就變成了傳統的通過阻塞來進行同步,並使用 monitor 物件來管理鎖的持有和釋放的方式(不要忘了 monitorenter 和 monitorexit 這兩個指令)。

相關文章