要點提煉| 理解JVM之線程安全&鎖優化

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

  • 概述
  • 線程安全
  • 鎖優化

1.概述數組

要點提煉| 理解JVM以內存模型&線程中主要介紹了虛擬機如何實現『併發』,如今的關注點是虛擬機如何實現『高效』。安全


2.線程安全多線程

在實現高效以前,首先須要保證併發的正確性,所以本節先介紹線程安全。併發

a.定義:當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象是線程安全的。函數

要求線程安全的代碼都必須具有一個特徵: 代碼自己封裝了全部必要的正確性保障手段(如互斥同步等),令調用者無須關心多線程的問題,更無須本身採起任何措施來保證多線程的正確調用。佈局

b.分類:按照線程安全的程度由強至弱分紅五類post

  • 不可變:外部的可見狀態永遠不會改變,在多個線程之中永遠是一致的狀態。
    • 必定是線程安全的
    • 如何實現
      • 若是共享數據是一個基本數據類型,只要在定義時用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能實現需求的狀況下,優先考慮使用它來進行同步。下兩張圖是二者在不一樣處理器上的吞吐量對比。

非阻塞同步(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分代年齡等。
  • 另一部分用於存儲指向方法區對象類型數據的指針,若是是數組對象還會有一個額外的部分用於存儲數組長度。

  • 加鎖過程:代碼進入同步塊時,若是同步對象未被鎖定(鎖標誌位爲01),虛擬機會在當前線程的棧幀中創建一個名爲Lock Record的空間,用於存儲鎖對象Mark Word的拷貝。以下圖。

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

另外,若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖,鎖標誌位變爲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的狀態,後續的同步操做就如輕量級鎖執行過程。以下圖。

  • 優勢:可提升帶有同步但無競爭的程序性能,但若程序中大多數鎖總被多個線程訪問,此模式就不必了。
相關文章
相關標籤/搜索