深入理解JVM(③)Java的鎖優化

前言

從JDK5到JDK6HotSpot虛擬機開發團隊花費了大量的資源實現了各種鎖優化技術,如適應性自旋(Adaptive Spinning)鎖消除(Lock Elimination)鎖膨脹(Lock Coarsening)輕量級鎖(LightEight Locking)偏向鎖(Biased Locking)等,這些技術都是胃了在線程之間更高效地共享數據及解決競爭問題,從而提供程序的執行效率。

自旋鎖與自適應鎖

在Java中鎖起到的作用是互斥同步,而互斥同步對性的影響最大的是阻塞,阻塞是通過掛起線程和恢復線程來實現的,這個操作是很昂貴的,消耗的服務器資源比較大。針對於此虛擬機開發團隊發明了自旋鎖,因爲在共享數據的鎖定狀態只會持續很短一段時間,爲了這段時間去掛起和恢復線程很不值得。所以在一個線程獲得鎖的同時另一個線程可以先“稍等一會兒”,但並不放棄處理器執行時間,爲了讓線程等待,只須讓線程執行一個忙循環(自旋),這就是自旋鎖。

那麼這個自旋鎖的自旋時間多久比較合適呢?

如自旋時間太短那就起不到自旋的作用了,太長又會佔用過多的處理器資源。所以在JDK1.4.2中引入自旋鎖的時候,就提供了自旋次數爲10默認值以及可以自行配置的參數-XXPreBlockSpin。

在JDK1.6中對自旋鎖進行了優化,引入了自適應自旋。它可以根據前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定的。如果上一次獲得了鎖,那麼下一次就會被認爲也會獲得鎖,進而自旋時間會加長;如果這個鎖很少被成功獲得,那麼有可能就直接省略掉自旋鎖,避免處理器資源浪費。

鎖消除

鎖消除是指:虛擬機即時編譯器在運行時,對一些代碼要求同步,但是對被檢測到不可能存在共享數據競爭的鎖進行消除。
鎖消除是虛擬機自行判斷的,開發人員,在編寫代碼的時候並不用刻意的去規避這些問題,因爲有些同步措施都是Java本身自己實現的。
例如如下代碼:

public String concatString(String str1,String str2,String str3){
    return str1 + str2 + str3;
}

因爲String是被final修飾的類,所以每次變動都是會產生新的String對象來進行的,因此在編譯時會對String連接做自動優化。在JDK5之前會轉成StringBuffer對象進行append()操作,在JDK5以後會轉爲StringBuilder對象進行append()操作。
這樣JDK5之前編譯器就會把代碼變成如下形式:

public String concatString(String str1,String str2,String str3){
    StringBuffer sb = new StringBuffer();
    sb.append(str1);
    sb.append(str2);
    sb.append(str3);
    return sb.toString();
}

因爲StringBuffer::append()方法就涉及到同步塊,鎖的就是sb對象。所以發現sb的動態作用域在concatString()方法內部,其他線程又無法訪問到它,因此這裏的鎖就可以被安全的消除。

鎖粗化

我們在編寫代碼的時候,一般會遵循一個原則,就是儘量將同步塊的作用範圍限制的最小,只在共享數據的實際作用域中才進行同步,這樣同步操作數量會變得更少,即使存鎖競爭,等待鎖的線程也能儘可能快地拿到鎖。
但是實際情況,在一系列連續操作都對同一個對象反覆加鎖和解鎖,甚至加鎖操作時出現在循環體之中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。
上面的代碼中concatString()方法就是頻繁的堆sb對象進行加鎖,虛擬機會探測到這種情況,將鎖的範圍擴展到整個系列操作的外部。就是在第一個append()操作之前到最後一個append()操作之後,只需要加一次鎖就可以了。
總結一下鎖粗化:虛擬機探測到有一系列零碎的操作都對同一個對象加鎖,將會加鎖的同步範圍擴展(粗化)到整個系列的操作外部。

輕量級鎖

輕量級鎖是相對於操作系統互斥量來實現的“重量級”鎖而言的,但是輕量級鎖並不用來替代重量級鎖的,它是指在沒有多線程競爭的前提下,減少重量級鎖使用操作系統互斥量產生的性能消耗。

要理解輕量級鎖,必須要對虛擬機對象的內存佈局(尤其是對象頭部分)。

HotSpot虛擬機的對象頭分爲兩部分:
  • 第一部門用戶存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡(Generational GC Age)等。這部分數據的長度咋32位和64位的虛擬機中分別會佔用32個或64個比特,官方稱它爲“Mark Word”,它是實現輕量級鎖和偏向鎖的關鍵
  • 第二部分是用於存儲指向方法區對象類型數據的指針,如果是數組對象,還會有一個額外的部分用戶存儲數組長度

由於對象頭信息是與對象自身定義的數據無關的額外存儲成本,Mark Word被設計成一個非固定的動態數據結構,以便在極小的空間內存儲儘量多的信息。
Mark Word會根據對象的狀態複用自己的存儲空間。下面是對象的狀態對應的對象頭的存儲內容表

HotSpot虛擬機對象頭Mark Word

輕量級鎖工作過程

輕量級鎖加鎖
  1. 在代碼即將進入同步塊的時候,如果此同步對象沒有被鎖定(標誌位“01”狀態),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝。
  2. 然後,虛擬機將使用CAS操作塵世把對象的Mark Word 更新爲執行Lock Record 的指針。
  3. 如果這個更新操作成功了,即代表線程擁有了這個對象的鎖,並且對象Mark Word的鎖標誌位(Mark Word的最後兩個比特)將轉變爲“00”,表示此對象處於輕量級鎖定狀態。
  4. 如果這個更新操作失敗了,那就意味着至少存在一條線程與當前線程競爭獲取該對象的鎖。

上面說了輕量級鎖的加鎖過程了,它的解鎖過程也同樣是通過CAS操作來進行的。

  1. 如果對象的Mark Word 仍然指向線程的鎖記錄,那就用CAS操作把對象當前的Mark Wrod和線程中複製的Displaced Mark Word替換回來。
  2. 加入能夠替換,那整個同步過程就順利完成了;
  3. 如果替換失敗,則說明有其他線程嘗試過濾獲取該鎖,就要在釋放鎖的同時,喚醒被掛起的線程。

輕量級鎖總結:
輕量級鎖能提升新恆信性能的依據是:“對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”。
如果沒有競爭,輕量級鎖便通過CAS操作成功避免了使用互斥量的開銷;但如果確實存在鎖競爭,除了互斥量的本身開銷外,還額外發生了CAS操作的開銷。因此在有競爭的情況下,輕量級鎖反而會比傳統的重量級鎖更慢。

偏向鎖

偏向鎖的意義:

偏向鎖的目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。
如果說輕量級鎖是在無競爭的情況下使用CAS操作消除同步使用的互斥量,那偏向鎖就是咋無競爭的情況下把整個同步都消除掉,連CAS操作都不去做了。

偏向鎖的定義:

這個鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖一直沒有被其他線程獲取,則持有偏向鎖的線程將用於不需要在進行同步。

偏向鎖加鎖過程

  1. 當虛擬機啓動了偏向鎖,那麼當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設置爲“01”、把偏向模式設置爲“1”,表示進入偏向模式。
  2. 同時使用CAS操作把獲取到這個鎖的線程ID記錄在對象的Mark Word之中。
  3. 如果CAS操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊是,虛擬機都可以不再進行任何同步操作。

偏向鎖解鎖過程

當出現另外一個線程區嘗試獲取這個鎖的情況,偏向模式就馬上宣告結束。根據鎖對象目前是否處於被鎖定的狀態決定是否撤銷偏向(偏向模式設置爲“0”),撤銷後標誌位恢復到未鎖定(標誌位“01”)或輕量級鎖定(標誌位爲“00”)的狀態,後續的同步操作就按照上面介紹的輕量級鎖那樣去執行。

感悟:

《深入理解Java虛擬機(第三版)》這本書,差不多算是看完了,筆記也都記錄下來了,還是有幾章個人覺得並不重要的章節給忽略掉了,所以就沒有做筆記。這次的感覺比第一次讀第二版的時候有了更深的理解,也接觸到了新的知識,例如以前沒研究過Java9的模塊化的知識。
這次陸陸續續算上在博客上做筆記也是大概用了一個多月將近2個月,感覺比讀第二版的時候,速度快了些。
每次準備換工作的時候,都是第一個想起來要看一遍這本書。但是最近兩年的情況好像有所不同了,現在各大互聯網公司面試必備的條件就是手撕算法,所以我也深刻的意識到了,想要出去面試光看這本書是遠遠不夠的,因此後續的時間裏,我將要開啓算法的學習歷程了,大家也一起加油吧!
還是那句話,我們只需努力,剩下的交給時間就好了。

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