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

正文

1、線程安全

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

一、Java 語言中的線程安全

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

(1)不可變

不可變的對象必定是線程安全的,不管是對象的方法實現仍是方法的調用者,都不須要再採起任何的線程安全保障措施。安全

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

(2)絕對線程安全

必須知足「無論運行時環境如何,調用者都不須要任何額外的同步措施」。多線程

實現代價很是大。併發

(3)相對線程安全

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

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

(4)線程兼容

指對象自己並非線程安全的,但能夠經過在調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用。優化

咱們日常說一個類不是線程安全的,絕大多數時候指的是這一種狀況。操作系統

(5)線程對立

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

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

二、線程安全的實現方法

(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)無同步方案

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

可重入代碼(純代碼):

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

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

線程本地存儲:

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

2、鎖優化

一、自旋鎖與自適應自旋

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

自適應自旋:

經過前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來肯定自旋的時間。若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,那麼虛擬機將容許自旋等待持續相對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後要獲取這個鎖時將可能省略掉自旋過程,以免浪費處理器資源。

二、鎖消除

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

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

三、鎖粗化

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

四、輕量級鎖

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

對象頭的 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 操做失敗,說明有其餘線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。

五、偏向鎖

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

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

偏向鎖鎖的執行過程:

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

結語

媽呀!終於寫完了~

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

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

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

唉,真是羞愧!

相關文章
相關標籤/搜索