《深入理解 Java 虛擬機》讀書筆記:線程安全與鎖優化

正文

一、線程安全

當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的。

1、Java 語言中的線程安全

按線程安全的“安全程度”由強至弱排序,可以將多個線程的共享數據分爲 5 類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。

(1)不可變

不可變的對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要再採取任何的線程安全保障措施。

  • 如果共享數據是一個基本數據類型,那麼只要在定義時使用 final 修飾就可以保證它是不可變的。
  • 如果共享數據是一個對象,那就需要保證對象的行爲不會對其狀態產生任何影響。最簡單的方法就是把對象中帶有狀態的變量都聲明爲 final,這樣在構造函數結束之後,它就是不可變的。

(2)絕對線程安全

必須滿足“不管運行時環境如何,調用者都不需要任何額外的同步措施”。

實現代價非常大。

(3)相對線程安全

就是我們通常意義上所講的線程安全,它需要保證對一個對象單獨的操作是線程安全的,調用時不需要額外的保障措施,但是對於一些特定順序的連續調用,可能需要在調用端使用額外的同步手段來保證調用的正確性。

Java 語言中,大部分的線程安全類都屬於這種類型。

(4)線程兼容

指對象本身並不是線程安全的,但可以通過在調用端正確地使用同步手段來保證對象在併發環境中可以安全地使用。

我們平常說一個類不是線程安全的,絕大多數時候指的是這一種情況。

(5)線程對立

指無論調用端是否採取了同步措施,都無法在多線程環境中併發使用的代碼。

Java 語言天生就具備多線程特性,線程對立這種排斥多線程的代碼是很少出現的。

2、線程安全的實現方法

(1)互斥同步(阻塞同步)

同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個(使用信號量時可以是多個)線程使用。而互斥是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實現方式。

從處理問題的方式上說,互斥同步屬於一種悲觀的併發策略,總是認爲只要不去做正確的同步措施(例如加鎖),那就肯定會出現問題。

synchronized:

synchronized 關鍵字是 Java 語言最基本的互斥同步手段,經過編譯後,它會在同步塊的前後分別形成 monitorenter 和 monitorexit 兩個字節碼指令,這兩個字節碼都需要一個 reference 類型的參數來指明要鎖定和解鎖的對象。

  • synchronized 同步塊對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題。
  • synchronized 同步塊在已進入的線程執行完之前,會阻塞後面其他線程的進入。

ReentrantLock(重入鎖):

java.util.concurrent 包中的 ReentrantLock 也可以用來實現同步,但相比 synchronized,ReentrantLock 增加了一些高級功能:

  • 等待可中斷:當持有鎖的線程長期不釋放鎖時,正在等待的線程可以選擇放棄等待,改爲處理其他事情。
  • 可實現公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。synchronized 中的鎖是非公平的,ReentrantLock 默認情況下也是非公平的,但可以通過帶布爾值的構造函數要求使用公平鎖。
  • 鎖可以綁定多個條件:一個 ReentrantLock 對象可以同時綁定多個 Condition 對象。而在synchronized 中,鎖對象的 wait() 和 notify()(或notifyAll())方法只能實現一個隱含的條件。

(2)非阻塞同步

非阻塞同步是一種樂觀的併發策略,它在進行同步操作時,不需要把線程掛起,而是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷地重試,直到成功爲止)。

CAS(比較並交換):

CAS 指令有 3 個操作數:變量的內存地址 V、舊的預期值 A、新值 B。當且僅當內存地址 V 的值與預期值 A 相等時,將內存地址 V 的值更新爲新值 B,否則不執行更新。上述的處理過程是一個原子操作。

CAS 的 ABA 問題:

如果一個變量 V 初次讀取的時候是 A 值,並且在準備賦值的時候檢查到它仍然爲 A 值,CAS 操作會認爲它從來沒有被改變過。但實際上,在這段期間它的值有可能曾經被改成了 B,後來又被改回爲 A。這個漏洞稱爲 CAS 操作的“ABA”問題。

(3)無同步方案

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

可重入代碼(純代碼):

如果一個方法,它的返回結果是可以預測的,只要輸入了相同的數據,就都能返回相同的結果,那它就滿足可重入性的要求,當然也就是線程安全的。

可重入代碼有一些共同的特徵,例如不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法等。

線程本地存儲:

如果能保證使用共享數據的代碼在同一個線程中執行,那麼就可以把共享數據的可見範圍限制在同一個線程之內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。

二、鎖優化

1、自旋鎖與自適應自旋

如果物理機有一個以上的處理器,能讓兩個或以上的線程同時並行執行,我們就可以讓後面請求鎖的那個線程“稍等一下”,但不放棄處理器的執行時間,然後看看持有鎖的線程是否很快就會釋放鎖。爲了讓線程等待,我們只需讓線程執行一個忙循環(自旋),這就是所謂的自旋鎖。

自適應自旋:

通過前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來確定自旋的時間。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,那麼虛擬機將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。

2、鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。

鎖消除的主要判定依據來源於逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把它們當做棧上數據對待,認爲它們是線程私有的,同步加鎖自然就無須進行。

3、鎖粗化

如果一系列的連續操作都對同一個對象反覆加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那麼虛擬機將會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部,這樣只需要加鎖一次就可以了。

4、輕量級鎖

輕量級鎖並不是用來代替重量級鎖的,而是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。

對象頭的 Mark Word 實現輕量級鎖和偏向鎖的關鍵。Mark Word 有一個 2bit 的鎖標誌位,用於標識對象的鎖狀態。

輕量級鎖的加鎖過程:

  1. 進入同步塊時,如果同步對象沒有被鎖定(鎖標誌位爲“01”狀態),虛擬機將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的 Mark Word 拷貝。
  2. 虛擬機使用 CAS 操作將對象的 Mark Word 更新爲指向 Lock Record 的指針。
  3. 如果 CAS 操作成功,那麼線程就擁有了該對象的鎖,並且對象 Mark Word 的鎖標誌位將轉變爲“00”,表示此對象處於輕量級鎖定狀態。
  4. 如果 CAS 操作失敗,虛擬機會檢查對象的 Mark Word 是否指向當前線程的棧幀,如果是說明當前線程已經擁有了該對象的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖對象已經被其他線程搶佔了。
  5. 如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word 中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。

輕量級鎖的解鎖過程:

  1. 如果對象 Mark Word 仍然指向線程的鎖記錄,那就用 CAS 操作把對象的 Mark Word 更新爲線程中的 Mark Word 拷貝。
  2. 如果 CAS 操作成功,整個同步過程就完成了。
  3. 如果 CAS 操作失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。

5、偏向鎖

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

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

偏向鎖鎖的執行過程:

  1. 鎖對象第一次被線程獲取時,虛擬機把對象 Mark Word 中的標誌位設爲“01”,即偏向模式。
  2. 使用 CAS 操作把獲取到這個鎖的線程 ID 記錄在對象的 Mark Word 中。
  3. 如果 CAS 操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛擬機都不再進行任何同步操作。
  4. 當有另外一個線程嘗試獲取這個鎖時,則結束偏向模式。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向後恢復到未鎖定(標誌位爲“01”)或輕量級鎖定(標誌位爲“00”)的狀態。

結語

媽呀!終於寫完了~

講道理,當初花三個月把這本書啃完的時候,本來還打算至少在過年的時侯把筆記整理完的。沒成想,只因爲看了一眼《半小時漫畫中國史》,頓時就覺得硬邦邦的技術書籍一點也不香了。直到我看完了三本《半小時漫畫中國史》才恍然驚醒——

唉?我只是個假的文藝青年啊,看這種書不太好吧?身爲假程序員的我,真是羞愧!

後來呢,因爲疫情的原因,過年的假期延長了一週。那段時間我就一直在想:我是不是忘記了什麼?然而,因爲我告訴我:做任何事情都要專注,開黑的時候不能分心。我覺得我說得挺有道理的,所以我也就沒太深究。後來我才知道,原來我得了失憶症,這種失憶症的名字叫做“事後自我欺騙型失憶症”。

唉,真是羞愧!

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