線程安全

一、什麼是線程安全?

當一個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方法進行任何其他協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的。 線程安全的代碼都必須具備一個特徵:代碼本身封裝了所有必要的正確性保障手段(如互斥同步等),令調用者無需關心多線程的問題,更無須自己採取任何措施來保證多線程的正確調用。

二、java語言中的線程安全

我們將java語言中各種操作共享的數據分爲以下5類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。

  1. 不可變 不可變(Immutable)的對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要再採取任何的線程安全保障措施,只要一個不可變的對象被正確地構建出來(沒有發生this引用逃逸的情況),那其外部的可見狀態永遠也不會改變,永遠也不會看到它在多個線程之中處於不一致的狀態。 例:被final修飾的變量,java.lang.String類的對象。
  2. 絕對線程安全 在Java API中標註自己是線程安全的類,大多數都不是絕對的線程安全。我們可以通過Java API中一個不是“絕對線程安全”的線程安全類來看看這裏的“絕對”是什麼意思。 例:java.util.Vector類,內部的方法是絕對線程安全的,所有方法都使用synchronized修飾。但是在實際調用中,也會出現不安全的情況,這和容器內代碼無關,是使用中出現的問題。
  3. 相對線程安全 相對的線程安全就是我們通常意義上所講的線程安全,它需要保證對這個對象單獨的操作是線程安全的,我們在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。 在Java語言中,大部分線程安全類都屬於這種類型,如Vector、HashTable、Collections的synchronizedCollection()方法包裝的集合等。
  4. 線程兼容 線程兼容是指對象本身不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在併發環境中可以安全地使用。 Java API中大部分的類都是屬於線程兼容的,如與前面的Vector和HashTable相對應的集合類ArrayList和HashMap等。
  5. 線程對立 線程對立是指無論調用端是否採取了同步措施,都無法在多線程環境中併發使用的代碼。線程對立的代碼通常都是有害的,應當儘量避免。 例如Thread類的suspend()和resume()方法,兩個線程同時調用容易產生死鎖,所以已經被jdk廢棄(@Deprecated)了。

三、線程安全的實現方法

如何實現線程安全和代碼編寫有很大的關係,但虛擬機如何實現同步和鎖機制也起到了非常重要的作用。本小節中重點介紹虛擬機如何實現同步與鎖。

1. 互斥同步

在多線程訪問的時候,保證同一時間只有一條線程使用。而互斥是實現同步的一種手段,臨界區(Critical Section),互斥量(Mutex),信號量(Semaphore)都是主要的互斥實現方式。

Java中實現互斥同步的兩種方法

  1. 使用synchronized實現同步,編譯之後會形成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都需要一個reference類型的參數來指明要鎖定和解鎖的對象。如果Java程序中的synchronized明確制定了對象參數,那就是這個對象的reference;如果沒有明確指定,那就根據synchronized修飾的是實例方法還是類方法,去取對應的對象實例或Class對象來作爲鎖對象。還有個鎖的計數器,來記錄擁有鎖的次數,跟AQS裏面的state一樣。
  2. 還可以使用java.util.concurrent(J.U.C)包中的重入鎖(ReentrantLock)來實現同步,在基本用法上,與synchronized很相似,他們都具備一樣的線程重入特性,只是代碼寫法上有點區別,一個表現爲API層面的互斥鎖(lock()和unlock()方法配合try/finally語句塊來完成),另一個表現爲原生語法層面的互斥鎖。不過,相比synchronized,ReentrantLock增加了一些高級功能,主要有以下3項:等待可中斷、可實現公平鎖,以及鎖可以綁定多個條件。
    • 等待可中斷是指當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改爲處理其他事情,可中斷特性對處理執行時間非常長的同步塊很有幫助。
    • 公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的線程都有機會獲得鎖。synchronized中的鎖是非公平的,ReentrantLock默認情況下也是非公平的,但可以通過帶布爾值的構造函數要求使用公平鎖。
    • 鎖綁定多個條件是指一個ReentrantLock對象可以同時綁定多個Condition對象,而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含的條件,如果要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock則無須這樣做,只需要多次調用newCondition()方法即可。

缺點

互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,因此這種同步也被稱爲阻塞同步(Blocking Synchronization)。互斥同步實際上是一種悲觀的併發策略。

2. 非阻塞同步

基於衝突檢測的樂觀併發策略,通俗地說,就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷地重試,直到成功爲止),這種樂觀的併發策略的許多實現都不需要把線程掛起,因此這種操作稱爲非阻塞同步(Non-Blocking Synchronization)。

樂觀併發策略需要“硬件指令集”,這類指令常用的有:

  • 測試並設置(Test-and-Set)
  • 獲取並增加(Fetch-and-Increment)
  • 交換(Swap)
  • 比較並交換(Compare-and-Swap,下文稱CAS)
  • 加載鏈接/條件存儲(Load-Linked/Store-Conditional,下文稱LL/SC)

CAS指令需要有3個操作數,分別是內存位置(在Java中可以簡單理解爲變量的內存地址,用V表示),舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,否則它就不更新,但是無論是否更新了V的值,都會返回V的舊值,上述處理過程是一個原子操作。

缺點

CAS操作的“ABA”問題: 如果一個變量V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然爲A值,那我們就能說它的值沒有被其他線程改變過了嗎?如果在這段期間它的值曾經被改成了B,後來又被改回爲A,那CAS操作就會誤認爲它從來沒有被改變過。 大部分情況下ABA問題不會影像程序併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。

3. 無同步方案

同步只是保證共享數據爭用時的正確性的手段,如果一個方法本來就不涉及共享數據,那它自然就無須任何同步措施去保證正確性,因此會有一些代碼天生就是線程安全的。

  • 可重入代碼(Reentrant Code):可重入代碼有一些共同的特徵,例如不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法等。
  • 線程本地存儲(Thread Local Storage):如果一段代碼中所需要的數據必須與其他代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行?如果能保證,我們就把共享數據的可見範圍限制在同一個線程之內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。 java中通過java.lang.ThreadLocal類來實現線程本地存儲功能。每一個線程的Thread對象中都有一個ThreadLocalMap對象,這個對象存儲了一組以ThreadLocal.threadLocalHashCode爲鍵,以本地線程變量爲值的K-V值對,ThreadLocal對象就是當前線程的ThreadLocalMap的訪問入口,每一個ThreadLocal對象都包含了一個獨一無二的threadLocalHashCode值,使用這個值就可以在線程K-V值對中找回對應的本地線程變量。

四、鎖優化

爲了在線程之間更高效的共享數據,以及解決競爭問題,從而提高程序的執行效率,創建了各種鎖優化技術:適應性自旋(Adaptive Spinning)、鎖消除(Lock Elimination)、 鎖粗化(Lock Coarsening)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)等,這些技術都是爲了在線程之間更高效地共享數據,以解決競爭問題,從而提高程序的執行效率。

1. 自旋鎖與自適應自旋

互斥同步對性能最大的影像是阻塞的實現,線程掛起和恢復的操作都需要轉入內核態中完成,這些操作給系統的併發性能帶來了很大的壓力。在許多應用中,共享數據的鎖定狀態只會持續很短的一段時間,爲了這段時間去掛起和恢復線程並不值得。可以讓後請求鎖的線程等待一會兒,但不放棄處理器的執行時間,讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。 自旋鎖默認的自旋次數值是10次,可以使用參數-XX:PreBlockSpin更改。 jdk1.6 引入了自適應的自旋鎖。意味着自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

2. 鎖消除

虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判定依據來源於逃逸分析的數據支持。

3. 鎖粗化

如果虛擬機探測到有一系列連續操作都對同一個對象反覆加鎖和解鎖,將會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部。

4. 輕量級鎖

使用對象頭的Mark Word中鎖標誌位代替操作系統互斥量實現的鎖。輕量級鎖並不是用來代替重量級鎖,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。輕量級鎖是在無競爭的情況下使用CAS(Compare-and-Swap)操作去消除同步使用的互斥量。

加鎖過程

  1. 在代碼進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標誌位爲“01”狀態),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Work的拷貝(即Displaced Mark Work),這時候線程堆棧與對象頭的狀態如下圖所示。
  1. 然後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record指針,並將Lock record裏的owner指針指向object mark word。
    • 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置爲“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態下圖所示。
    • 如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是爲了不讓線程阻塞,而採用循環去獲取鎖的過程。

解鎖過程

如果對象的Mark Work仍然指向着線程的鎖記錄,就通過CAS操作嘗試把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來。

  • 如果替換成功,整個同步過程就完成了。
  • 如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。

注意事項

輕量鎖能提升程序同步性能的依據是:對於絕大部分的鎖,在整個同步週期內都是不存在競爭的。所以,在沒有競爭的情況下,輕量級鎖使用CAS操作避免了使用互斥量的開銷。但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。

5. 偏向鎖

引入偏向鎖是爲了在無多線程競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因爲輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小於節省下來的CAS原子指令的性能消耗)。上面說過,輕量級鎖是爲了在線程交替執行同步塊時提高性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提高性能。

加鎖過程

  1. 當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標識位設爲“01”,即偏向模式。
  2. 然後使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Work之中。如果CAS操作成功,持有偏向鎖的線程以後每次進入和退出這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作(例如Locking、Unlocking及對Mark Work的Update等), 只需簡單測試一下對象頭的Mark Word裏是否存儲着指向當前線程線程的偏向鎖。

偏向鎖的撤銷

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲“01”)或輕量級鎖(標誌位爲“00”)的狀態。

注意事項

偏向鎖可以提高帶有同步但無競爭的程序性能。如果程序中大多數的鎖總是被多個不同的線程訪問,那偏向模式就是多餘的,可以使用參數-XX:-UseBiasedLocking來禁止偏向鎖優化。

輕量級鎖、偏向鎖的狀態轉化

輕量級鎖、偏向鎖的狀態轉化及Mark Work的關係圖

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