1. 程式人生 > >《深入理解Java虛擬機器》學習筆記之執行緒安全與鎖優化

《深入理解Java虛擬機器》學習筆記之執行緒安全與鎖優化

二、執行緒安全

  • 定義

    • “當多個執行緒訪問一個物件時,如果不用考慮這些執行緒在執行時環境下的排程和交替執行,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那這個物件是執行緒安全的” ——Brian Goetz《Java Concurrency In Practice》
  • 執行緒安全程式碼的特徵

    • 程式碼本身封裝了所有必要的正確性保障手段(如互斥同步等),令呼叫者無需關心多執行緒的問題,更無需自己採取任何措施來保證多執行緒的正確呼叫
1、Java語言中的執行緒安全(5類資料)
  • “執行緒安全”問題的討論物件:Java語言中各種操作共享的資料

    • “安全程度”由強至弱:不可變、絕對執行緒安全、相對執行緒安全、執行緒相容和執行緒對立
  • (1)不可變

    • 特徵:不可變物件一定是執行緒安全的

      • 無論是物件的方法實現還是方法的呼叫者,都不需要再採取任何的執行緒安全保障措施
    • 實現:使用final關鍵字

      • 對於基本資料型別:定義時使用final修飾即可
      • 對於引用資料型別(物件):需要保證物件的行為不會對其狀態產生任何影響 -> 將物件中帶有狀態的變數都宣告為final
  • (2)絕對執行緒安全

    • 特徵:完全滿足執行緒安全的定義

    • 實現:一般很難滿足

  • (3)相對執行緒安全

    • 特徵:

      • 是通常意義上所講的執行緒安全
      • 它需要保證對這個物件單獨的操作是執行緒安全的,在呼叫的時候不需要做額外的保障措施
      • 但是對於一些特定順序的連續呼叫,就可能需要在呼叫端使用額外的同步手段來保證呼叫的正確性
    • 實現:

      • Java API中標註的大多數執行緒安全的類都不是絕對執行緒安全,而是相對執行緒安全。它們雖然在內部做了同步處理,呼叫時有的仍需要額外的同步措施

      • eg:Vector是執行緒安全的容器,它的add()get()size()這類方法都是被synchronized修飾的,但在呼叫時仍要同步

        • 比如在另一個執行緒恰好在錯誤的時間裡刪除了一個元素,導致序號i已經不再可用,再用i訪問陣列就會丟擲ArrayIndexOutOfBoundsException
  • (4)執行緒相容

    • 特徵:指物件本身並不是執行緒安全的,但是可以通過在呼叫端正確的使用同步手段來保證物件在併發環境中可以安全使用

    • 實現:

      • 一般說一個類不是執行緒安全的,一般就是這種情況

      • Java API中大部分的類都是屬於執行緒相容的

        • eg:ArrayList和HashMap
  • (5)執行緒對立

    • 特徵:無論呼叫端是否採取同步措施,都無法在多執行緒環境中併發使用的程式碼

    • 實現:

      • 由於Java語言天生就具備多執行緒特性,執行緒對立這種排斥多執行緒的程式碼應儘量避免

      • eg:Thread類的suspend()(已廢棄)和resume()方法。如果有兩個執行緒同時持有一個執行緒物件,一個嘗試去中端執行緒,另一個嘗試去恢復執行緒,如果併發進行,無論是否同步,目標執行緒都存在死鎖的風險(suspend()的中斷和resume()的恢復是同一個執行緒的情況)

2、執行緒安全的實現方法(3種同步方案)
  • (1)互斥同步(阻塞同步)

    • 定義:是常見的一種併發正確性保障手段。互斥是因,同步是果;互斥是方法,同步是目的。

      • 同步:指在多個執行緒併發訪問共享資料時,保證共享資料是同一個時刻只被一個(或者是一些,使用訊號量的時候)執行緒使用

      • 互斥:實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和訊號量(Semaphore)都是主要的互斥實現方式

    • 缺點:

      • 進行執行緒阻塞和喚醒會帶來效能問題
    • 實現:

      • 1⃣ synchronized關鍵字
      • 2⃣ java.util.concurrent包中的重入鎖(ReentrantLock):
    • synchronized實現過程:

      • synchronized關鍵字經過編譯之後,會在同步塊前後分別形成monitorentermonitorexit兩個位元組碼指令

      • 這兩個位元組碼都需要一個reference型別的引數來指明要鎖定和解鎖的物件

        情境 reference引數
        Java程式中的synchronized明確指明瞭物件引數 被指明物件的reference
        未明確指定物件引數 根據synchronized修飾的是例項方法還是類方法,去獲取對應的物件例項或Class物件來作為鎖物件
      • monitorenter指令執行前需要先嚐試獲取物件的鎖:

        情境 處理
        如果這個物件沒被鎖定,或者當前執行緒已經擁有了那個物件的鎖 將鎖的計數器 +1
        在執行monitorexit指令時 將鎖的計數器 -1
        當計數器為 0 時 鎖被釋放
        獲取物件鎖失敗 當前執行緒要阻塞等待,直到物件鎖被另一個執行緒釋放
    • monitorentermonitorexit的行為描述注意點:

      • 1⃣ synchronized同步塊對同一條執行緒來說是可重入的,不會出現自己把自己鎖死的問題

      • 2⃣ 同步塊在已進入的執行緒執行完之前,會阻塞後面其他執行緒的進入

    • synchronized是Java語言中一個重量級(Heavyweight)的操作

      • 原因:Java 的執行緒是對映到作業系統的原生執行緒上的,如果要阻塞或喚醒一個執行緒,都需要作業系統來幫忙完成,這就需要從使用者態轉換到核心態中,而狀態轉換需要耗費很多的處理器時間,有可能比使用者程式碼執行的時間還長
    • 可重入鎖(ReentrantLock)新增多個高階功能

      名稱 定義 特性 / 例項
      等待可中斷 指當持有鎖的執行緒長期不是放鎖的時候,正在等待的執行緒可以選擇放棄等待,改為處理其他事情 此特性對處理執行時間非常常的同步塊很有幫助
      公平鎖 指多個執行緒在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖(非公平鎖則不能保證這一點,在鎖被釋放時,任何一個等待鎖的執行緒都有機會獲得鎖) synchronized中的鎖是非公平的,ReentrantLock 預設情況下也是非公平的,但可以通過帶布林值的建構函式要求使用公平鎖
      鎖繫結多個條件 指一個 ReentrantLock 物件可以同時繫結多個 Condition 物件 synchronized中,鎖物件的wait()notify()notifyAll()方法可以實現一個隱含的條件,如果要和多於一個的條件關聯的時候,就不得不額外新增一個鎖,而 ReentrantLock 只需多次呼叫newCondition()方法即可
  • (2)非阻塞同步

    • 引出:互斥同步是一種悲觀併發策略,總是認為如果不去做正確的同步措施就會出問題

      • 無論共享資料是否真的會出現競爭,它都要進行加鎖、使用者核心態轉換、維護鎖計數器和檢查是否有被阻塞的執行緒需要喚醒等操作
    • 基於衝突檢測的樂觀併發策略

      • 前提:硬體指令集的發展(需要操作和衝突檢測這兩個操作具備原子性 -> 依靠硬體保證一個從語義上看起來需要多次操作的行為指通過一條處理器指令就能完成 -> CAS
        指令 (目前仍存在“ABA”問題)

      • 內容:先進行操作,如果沒有其他執行緒振勇共享資料,那操作就成功;如果共享資料有爭用,那就再採取其他的補償措施(最常見的補償措施是不斷重試,直到成功)

      • 結果:這種實現不需要把執行緒掛起,因此這種同步操作也被稱作非阻塞同步(Non-Blocking Synchronization)

  • (3)無同步方案(不涉及共享資料的程式碼天生就是執行緒安全的)

    • 1⃣ 可重入程式碼(Reentrant Code)

      • 定義:這種程式碼也叫做純程式碼(Pure Code),可以在程式碼執行的任何時刻中斷它,轉而去執行另外一段程式碼(包括遞迴呼叫它自身),而在控制權返回後,原來的程式不會出現任何錯誤

      • 特徵:如果一個方法,它的返回結果是可以預測的,只要輸入了相同的資料,就都能返回相同的結果,那它就滿足可重入性的要求,當然也就是執行緒安全的

    • 2⃣ 執行緒本地儲存(Thread Local Storage)

      • 定義:如果一段程式碼中所需要的資料必須與其他程式碼共享,且這些共享程式碼能保證在同一個執行緒中執行,那麼就可以把共享資料的可見範圍限制在同一個執行緒之內

      • 實現:java.lang.ThreadLocal類 -> 每一個執行緒的 Thread 物件中都有一個ThreadLocalMap物件,這個物件儲存了一組以ThreadLocal.threadLocalHashCode為鍵,以本地執行緒變數為值的鍵值對

三、鎖優化

  • 鎖優化的目的:為了線上程之間更高效的共享資料,以及解決競爭問題
1、自旋鎖與自適應自旋
  • 自旋鎖概念理解

    • 問題引出:互斥同步對效能最大的影響是阻塞的實現,掛起執行緒和恢復執行緒的操作都需要轉入核心態中完成,會給系統的併發效能帶來很大壓力

      • 實踐證明共享資料的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復執行緒並不值得
    • 思路:如果物理機器有一個以上的處理器,能讓兩個或以上的執行緒同時執行,就可以讓後面請求鎖的那個執行緒“稍等一下”,但不放棄處理器的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖

    • 解決:為了讓執行緒等待,只需讓執行緒執行一個忙迴圈(自旋),這項技術就是自旋鎖

  • 自旋鎖的特性(優缺點)及使用時機

    • 特性:

      • 自旋鎖預設是關閉的

      • 自旋等待不能代替阻塞

      • 自旋等待的時間是有一定限度的,如果自旋超過了限定次數仍然沒有成功獲得鎖,就應當使用傳統方式掛起執行緒(預設自旋次數為10次)

    • 優點:

      • 自旋等待避免了執行緒切換的開銷
    • 缺點:

      • 自旋等待要佔用處理器時間
    • 使用時機:

      • 如果鎖被佔用的時間很短,自旋等待效果很好
      • 如果鎖被佔用的時間很長,那麼自旋執行緒只會白白消耗處理器資源,帶來效能傷的浪費(所以有自旋限度的概念)
  • 自適應自旋

    • 定義:自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定

      情境 結果
      如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒中正在執行中 那麼虛擬機器就會認為這次自旋也很有可能再次成功,進而他將允許自旋等待持續相對更長的時間(大於10次)
      如果對於某個鎖,自旋很少成功獲得過 那麼在以後要獲取這個鎖時可能省略掉自旋過程,以避免浪費處理器資源
2、鎖消除
  • 定義

    • 指虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被監測到不可能存在共享資料競爭的鎖進行消除
  • 判定依據

    • 逃逸分析的資料支援:如果判斷在一段程式碼中,堆上的所有資料都不會逃逸出去從而被其他執行緒訪問到,那就可以把他們當作棧上資料對待,認為他們是執行緒私有的
3、鎖粗化
  • 定義:如果虛擬機器探測到有一系列連續的操作都對同一個物件反覆加鎖和解鎖,將會把加鎖同步的範圍擴充套件(粗化)到整個操作序列的外部,只加鎖一次
    • 避免頻繁的進行互斥同步操作導致的不必要效能損耗
4、輕量級鎖
  • 目的:在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗(提升同步效能)

    • 輕量級鎖並不是用來代替傳統的鎖機制的(重量級鎖)
    • 依據: “對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”
      • 這是一個經驗資料。如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了 CAS 操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。
  • 加鎖過程:與物件頭的Mark Word相關

    image

    • 1⃣ 在程式碼進入同步塊的時候,如果此同步物件沒有被鎖定(鎖標誌位為“01”狀態),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的 Mark Word 拷貝(官方把這份拷貝加上了一個 Displaced 字首,即 Displaced Mark Word)

      image

    • 2⃣ 然後,虛擬機器將使用 CAS 操作嘗試將物件的 Mark Word 更新為指向 Lock Record 的指標。如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件 Mark Word 的鎖標誌位 (Mark Word 的最後 2bit)將轉變為 “00”,即表示此物件處於輕量級鎖定狀態,這時候執行緒堆疊與物件頭的狀態如圖 12-4 所示。

      image

    • 3⃣ 如果這個更新操作失敗了,虛擬機器首先會檢查物件的 Mark Word 是否指向當前執行緒的棧幀

      • 如果只說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖物件以及被其他執行緒執行緒搶佔了。
      • 如果有兩條以上的執行緒爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,所標誌的狀態變為 “10”,Mark Word 中儲存的就是指向重量級鎖(互斥量)的指標,後面等待鎖的執行緒也要進入阻塞狀態。
  • 解鎖過程:通過 CAS 操作來進行

    • 如果物件的 Mark Word 仍然指向著執行緒的鎖記錄,那就用 CAS 操作把物件當前的 Mark Word 和執行緒中複製的 Displaced Mark Word 替換回來
      • 如果替換成功,整個同步過程就完成了
      • 如果替換失敗,說明有其他執行緒嘗試過獲取該鎖,那就要釋放鎖的同時,喚醒被掛起的執行緒
  • CAS(Compare and Swap):比較並交換

    • Java中的同步器是基於CAS技術實現的
5、偏向鎖
  • 目的:消除資料在無競爭情況下的同步原語,進一步提高程式的執行效能

    • 如果說輕量級鎖是在無競爭的情況下使用 CAS 操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連 CAS 操作都不做了。
  • “偏”的理解

    • 它的意思是這個鎖會偏向於第一個獲得它的執行緒
    • 如果在接下來的執行過程中,該鎖沒有被其他的執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步
  • 原理

    • 假設當前虛擬機器啟用了偏向鎖(啟用引數 -XX:+UseBiasedLocking,這是 JDK 1.6 的預設值),那麼,當鎖物件第一次被執行緒獲取的時候,虛擬機器將會把物件頭中的標誌位設為 “01”,即偏向模式。同時使用 CAS 操作把獲取到這個鎖的執行緒 ID 記錄在物件的 Mark Word 之中

      • 如果 CAS 操作成功,持有偏向鎖的執行緒以後每次進入這個鎖相關的同步塊時,虛擬機器都可以不再進行如何同步操作(例如 Locking、Unlocking 及對 Mark Word 的 Update 等)。
    • 當有另外一個執行緒去嘗試獲取這個鎖時,偏向模式就宣告結束。

    • 根據鎖物件目前是否處於被鎖定的狀態,撤銷偏向(Revoke Bias)後恢復到未鎖定(標誌位為 “01”)或輕量級鎖定(標誌位為 “00”)的狀態

    • 後續的同步操作就如輕量級鎖執行

  • 偏向鎖、輕量級鎖的狀態轉換及物件 Mark Word 的關係

    image

  • 優缺點

    • 它同樣是一個帶有效益權衡(Trade Off)性質的優化

      • 它並不一定總是對程式執行有利
    • 優點:偏向鎖可以提高帶有同步但無競爭的程式效能

    • 缺點:如果程式中大多數的鎖總是被多個不同的執行緒訪問,那偏向模式就是多餘的。使用引數 -XX:-UseBiasedLocking 來禁止偏向鎖優化反而可以提升效能