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

紀莫發表於2020-07-26

前言

從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個月,感覺比讀第二版的時候,速度快了些。
每次準備換工作的時候,都是第一個想起來要看一遍這本書。但是最近兩年的情況好像有所不同了,現在各大網際網路公司面試必備的條件就是手撕演算法,所以我也深刻的意識到了,想要出去面試光看這本書是遠遠不夠的,因此後續的時間裡,我將要開啟演算法的學習歷程了,大家也一起加油吧!
還是那句話,我們只需努力,剩下的交給時間就好了。

相關文章