談一談 JVM 對鎖的優化 一. 從 ReentrantLock 和 synchronized 看鎖的優化 二. 自旋鎖 三. 鎖消除 四. 鎖粗化 五. 偏向鎖 六. 輕量級鎖

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 這兩個指令)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章