要點提煉| 理解JVM之執行緒安全&鎖優化

釐米姑娘發表於2018-07-19

本篇將介紹執行緒安全所涉及的概念和分類、同步實現的方式及虛擬機器的底層運作原理,以及虛擬機器為了實現高效併發所採取的一系列鎖優化措施。

  • 概述
  • 執行緒安全
  • 鎖優化

1.概述

要點提煉| 理解JVM之記憶體模型&執行緒中主要介紹了虛擬機器如何實現『併發』,現在的關注點是虛擬機器如何實現『高效』。


2.執行緒安全

在實現高效之前,首先需要保證併發的正確性,因此本節先介紹執行緒安全。

a.定義:當多個執行緒訪問一個物件時,如果不用考慮這些執行緒在執行時環境下的排程和交替執行,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那這個物件是執行緒安全的。

要求執行緒安全的程式碼都必須具備一個特徵: 程式碼本身封裝了所有必要的正確性保障手段(如互斥同步等),令呼叫者無須關心多執行緒的問題,更無須自己採取任何措施來保證多執行緒的正確呼叫。

b.分類:按照執行緒安全的程度由強至弱分成五類

  • 不可變:外部的可見狀態永遠不會改變,在多個執行緒之中永遠是一致的狀態。
    • 一定是執行緒安全的
    • 如何實現
      • 如果共享資料是一個基本資料型別,只要在定義時用final關鍵字修飾;
      • 如果共享資料是一個物件,最簡單的方法是把物件中帶有狀態的變數都宣告為final
  • 絕對執行緒安全:完全滿足之前給出的執行緒安全的定義,即達到“不管執行時環境如何,呼叫者都不需要任何額外的同步措施”。
  • 相對執行緒安全:能保證對該物件單獨的操作是執行緒安全的,在呼叫時無需做額外保障措施,但對於一些特定順序的連續呼叫,可能需要在呼叫端使用額外的同步措施來保證呼叫的正確性。
    • 是通常意義上所講的執行緒安全
    • 大部分的執行緒安全類都屬於這種型別,如VectorHashTableCollections#synchronizedCollection()包裝的集合...
    • 有關實現在下一小節細說。
  • 執行緒相容:物件本身非執行緒安全的,但可以通過在呼叫端正確地使用同步手段來保證物件在併發環境中可以安全地使用,
    • 是通常意義上所講的非執行緒安全
    • Java API中大部分類都是屬於執行緒相容的,如ArrayListHashMap...
  • 執行緒對立:無論呼叫端是否採取了同步措施,都無法在多執行緒環境中併發使用的程式碼。

c.執行緒安全的實現

可分成兩大手段,本篇重點在虛擬機器本身

  • 通過程式碼編寫實現執行緒安全
  • 通過虛擬機器本身實現同步與鎖

互斥同步(Mutual Exclusion&Synchronization)

  • 含義
    • 同步:在多個執行緒併發訪問共享資料時,保證共享資料在同一個時刻只被一個執行緒使用。
    • 互斥:是實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和訊號量(Semaphore)都是主要的互斥實現方式。

互斥是因,同步是果;互斥是方法,同步是目的。

  • 屬於悲觀併發策略,即認為只要不做正確的同步措施就肯定會出現問題,因此無論共享資料是否真的會出現競爭,都要加鎖。
  • 最大的問題是進行執行緒阻塞和喚醒所帶來的效能問題,也稱為阻塞同步(Blocking Synchronization)
  • 手段
    • 使用synchronized關鍵字:
      • 原理:編譯後會在同步塊的前後分別形成monitorentermonitorexit這兩個位元組碼指令,並通過一個reference型別的引數來指明要鎖定和解鎖的物件。若明確指定了物件引數,則取該物件的reference;否則,會根據synchronized修飾的是例項方法還是類方法去取對應的物件例項或Class物件來作為鎖物件。
      • 過程:執行monitorenter指令時先要嘗試獲取物件的鎖。若該物件沒被鎖定或者已被當前執行緒獲取,那麼鎖計數器+1;而在執行monitorexit指令時,鎖計數器-1;當鎖計數器=0時,鎖就被釋放;若獲取物件鎖失敗,那當前執行緒會一直被阻塞等待,直到物件鎖被另外一個執行緒釋放為止。
      • 特別注意synchronized同步塊對同一條執行緒來說是可重入的,不會出現自我鎖死的問題;還有,同步塊在已進入的執行緒執行完之前,會阻塞後面其他執行緒的進入。
    • 使用重入鎖ReentrantLock
      • 相同:用法與synchronized很相似,且都可重入。
      • synchronized不同
        • 等待可中斷:當持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待,改為處理其他事情。
        • 公平鎖:多個執行緒在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。而synchronized是非公平的,即在鎖被釋放時,任何一個等待鎖的執行緒都有機會獲得鎖。ReentrantLock預設情況下也是非公平的,但可以通過帶布林值的建構函式改用公平鎖。
        • 鎖繫結多個條件:一個ReentrantLock物件可以通過多次呼叫newCondition()同時繫結多個Condition物件。而在synchronized中,鎖物件的wait()notify()notifyAl()只能實現一個隱含的條件,若要和多於一個的條件關聯不得不額外地新增一個鎖。
      • 選擇:在synchronized能實現需求的情況下,優先考慮使用它來進行同步。下兩張圖是兩者在不同處理器上的吞吐量對比。

要點提煉| 理解JVM之執行緒安全&鎖優化

要點提煉| 理解JVM之執行緒安全&鎖優化

非阻塞同步(Non-Blocking Synchronization):

  • 基於衝突檢測的樂觀併發策略,即先進行操作,若無其他執行緒爭用共享資料,操作成功;反之產生了衝突再去採取其他的補償措施。
  • 為了保證操作衝突檢測這兩步具備原子性,需要用到硬體指令集,比如:
    • 測試並設定(Test-and-Set)
    • 獲取並增加(Fetch-and-Increment)
    • 交換(Swap)
    • 比較並交換(Compare-and-Swap,CAS)
    • 載入連結/條件儲存(Load-Linked/Store-Conditional,LL/SC)

無同步方案

  • 定義:不用同步的方式保證執行緒安全,因為有些程式碼天生就是執行緒安全的。下面舉兩個例子:
  • 可重入程式碼(Reentrant Code)/純程式碼(Pure Code)
    • 含義:可在程式碼執行的任何時刻中斷它去執行另外一段程式碼,當控制權返回後原來的程式並不會出現任何錯誤。
    • 共同特徵:不依賴儲存在堆上的資料和公用的系統資源、用到的狀態量都由引數中傳入、不呼叫非可重入的方法...
    • 判定依據:如果一個方法,它的返回結果是可預測的,只要輸入相同的資料就都能返回相同的結果,就滿足可重入性。

滿足可重入性的程式碼一定是執行緒安全的,反之,滿足執行緒安全的程式碼不一定是可重入的。

  • 執行緒本地儲存(Thread Local Storage)
    • 含義:把共享資料的可見範圍限制在同一個執行緒之內,無須同步就能保證執行緒之間不出現資料爭用的問題。
    • 使用ThreadLocal類可實現執行緒本地儲存的功能:每個執行緒的Thread物件中都有一個ThreadLocalMap物件,它儲存了一組以ThreadLocal.threadLocalHashCode為key、以本地執行緒變數為value的鍵值對,而ThreadLocal物件就是當前執行緒的ThreadLocalMap的訪問入口,也就包含了一個獨一無二的threadLocalHashCode值,通過這個值就可以線上程鍵值值對中找回對應的本地執行緒變數。

3.鎖優化

解決併發的正確性之後,為了能線上程之間更『高效』地共享資料、解決競爭問題、提高程式的執行效率,下面介紹五種鎖優化技術。

a.適應性自旋(Adaptive Spinning)

  • 背景:互斥同步在實現阻塞和喚醒時需要掛起執行緒和恢復執行緒的操作,都需要轉入核心態中完成,很影響系統的併發效能;同時,在許多應用上共享資料的鎖定狀態只是暫時,沒必要去掛起和恢復執行緒。
  • 自旋鎖:當物理機器有多個處理器使得多個執行緒同時並行執行時,先讓後請求鎖的執行緒等待,但不放棄處理器的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖,這時只需讓執行緒執行一個忙迴圈,即自旋。
    • 注意:自旋等待不能代替阻塞,它雖然能避免執行緒切換的開銷,但會佔用處理器時間,因此自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍未成功獲鎖,就需要掛執行緒了。
  • 自適應自旋鎖:自旋的時間不再固定,而是由該鎖上的上次自旋時間及鎖的擁有者的狀態共同決定。具體表現是:
    • 如果對於某個鎖,自旋等待剛剛成功獲得,且持有鎖的執行緒正在執行中,那麼虛擬機器很可能允許自旋等待的時間更久點。
    • 如果對於某個鎖,自旋很少成功獲得過,那麼很可能以後將省略自旋等待這個鎖,避免浪費處理器資源。

b.鎖消除(Lock Elimination)

  • 鎖消除:指虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行消除。
  • 判定依據:如果一段程式碼中上的所有資料都不會逃逸出去被其他執行緒訪問到,可把它們當做上資料對待,即執行緒私有的,無須同步加鎖。

c.鎖粗化(Lock Coarsening)

一般情況下,會將同步塊的作用範圍限制到只在共享資料的實際作用域中才進行同步,使得需要同步的運算元量儘可能變小,保證就算存在鎖競爭,等待鎖的執行緒也能儘快拿到鎖。

但如果反覆操作對同一個物件進行加鎖和解鎖,即使沒有執行緒競爭,頻繁地進行互斥同步操作也會導致不必要的效能損耗,此時,虛擬機器將會把加鎖同步的範圍粗化到整個操作序列的外部,這樣只需加一次鎖。

d.輕量級鎖(Lightweight Locking)

  • 目的:在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗,注意不是用來代替重量級鎖的。

首先先理解HotSpot虛擬機器的物件頭的記憶體佈局:分為兩部分

  • 第一部分用於儲存物件自身的執行時資料,這部分被稱為Mark Word,是實現輕量級鎖和偏向鎖的關鍵。如雜湊碼、GC分代年齡等。
  • 另外一部分用於儲存指向方法區物件型別資料的指標,如果是陣列物件還會有一個額外的部分用於儲存陣列長度。

要點提煉| 理解JVM之執行緒安全&鎖優化

  • 加鎖過程:程式碼進入同步塊時,如果同步物件未被鎖定(鎖標誌位為01),虛擬機器會在當前執行緒的棧幀中建立一個名為Lock Record的空間,用於儲存鎖物件Mark Word的拷貝。如下圖。

要點提煉| 理解JVM之執行緒安全&鎖優化

之後虛擬機器會嘗試用CAS操作將物件的Mark Word更新為指向Lock Record的指標。若更新動作成功,那麼當前執行緒就擁有了該物件的鎖,且物件Mark Word的鎖標誌位變為00,即處於輕量級鎖定狀態;反之,虛擬機器會先檢查物件的Mark Word是否指向當前執行緒的棧幀,若當前執行緒已有該物件的鎖,可直接進入同步塊繼續執行,否則說明改物件已被其他執行緒搶佔。如下圖。

要點提煉| 理解JVM之執行緒安全&鎖優化

另外,如果有兩條以上的執行緒爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標誌位變為10,Mark Word中儲存的就是指向重量級鎖的指標,後面等待鎖的執行緒也要進入阻塞狀態。

  • 解鎖過程:若物件的Mark Word仍指向著執行緒的Lock Record,就用CAS操作把物件當前的Mark Word和執行緒中複製的Displaced Mark Word替換回來。若替換成功,那麼就完成了整個同步過程;反之,說明有其他執行緒嘗試獲取該鎖,那麼就要在釋放鎖的同時喚醒被掛起的執行緒。
  • 優點:因為對於絕大部分的鎖,在整個同步週期內都是不存在競爭的,所以輕量級鎖通過使用CAS操作消除同步使用的互斥量。

e.偏向鎖(Biased Locking)

  • 目的:消除資料在無競爭情況下的同步原語,進一步提高程式的執行效能。
  • 含義:偏向鎖會偏向於第一個獲得它的執行緒,如果在後面的執行中該鎖沒有被其他的執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。
  • 加鎖過程:啟用偏向鎖的鎖物件在第一次被執行緒獲取時,Mark Word的鎖標誌位會被設定為01,即偏向模式,同時使用CAS操作把獲取到這個鎖的執行緒ID記錄在物件的Mark Word中。若操作成功,持有偏向鎖的執行緒以後每次進入這個鎖相關的同步塊時都可不再進行任何同步操作。
  • 解鎖過程:當有另外的執行緒去嘗試獲取這個鎖時,根據鎖物件目前是否處於被鎖定的狀態,撤銷偏向後恢復到未鎖定01或輕量級鎖定00的狀態,後續的同步操作就如輕量級鎖執行過程。如下圖。

要點提煉| 理解JVM之執行緒安全&鎖優化

  • 優點:可提高帶有同步但無競爭的程式效能,但若程式中大多數鎖總被多個執行緒訪問,此模式就沒必要了。

相關文章