第二章 Java併發機制的底層實現原理

1. volatile定義與實現原理

1.1 定義

    確保共享變量能夠被準確和一致的更新,線程應該確保通過排他鎖單獨獲得這個變量,若一個字段被聲明爲volatile,Java線程內存模型確保所有線程看到這個變量的值時一致的。

1.2 當volatile修飾變量,CPU會做什麼事情?

比如:volatile instance = new Singleton();轉變成彙編代碼如下:
0X01a3d1d:movb $0x0, 0x1104800(%esi); 0x01a3de24: lock add1 $0x0, (%esp)

主要做了兩件事:

1)將當前處理器緩存行的數據寫回到系統內存

2)這個寫回內存的操作會使在其他CPU裏緩存了該內存地址的數據無效

1.3 使用優化

如何做:追加字節能優化性能,將共享變量追加到64字節

爲什麼:對於酷睿I7和Pentium M處理器的L1、L2或L3緩存的高速緩存行是64個字節寬,不支持部分填充緩存行,若頭節點和尾節  點都不足64字節,處理器會將他們讀到同一個高速緩存行中,每個處理器都會緩存同樣的頭、尾節點,當一個節點試圖修改頭節點時,會鎖定整個緩存行,在緩存一致性機制的作用下,會導致其他處理器不能訪問自己高速緩存中的尾節點

例外情況:1)緩存行非64字節寬的處理器。2)共享變量不會被頻繁的寫

2. Synchronized的實現原理與應用

2.1 表現

1)對於普通同步方法,鎖是當前實例對象

2)對於靜態同步方法,鎖是當前類的Class對象

3)對於同步方法塊,鎖是Synchronized括號裏配置的對象

2.2 概述

2.2.1 實現原理

    JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,代碼塊同步是使用monitorenter和monitorexit指令實現的

2.2.2 流程

    monitorenter指令實在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM保證每個monitorenter和monitorexit與之配對,任何對象都有一個monitor與之關聯,當且一個Moniter被持有後,就會處於鎖定狀態。線程執行到monitorenter指令時,就會嘗試獲取對象所對應的monitor的所有權,即對象鎖

2.3 具體實現

2.3.1 實現

1)同步代碼塊採用monitorenter、monitorexit指令顯示的實現

2)同步方法則使用ACC_synchronized標記符隱式的實現

2.3.2 具體實現方法

2.3.2.1 monitorenter

描述:

    每一個對象都有一個monitor,一個monitor只能被一個線程有用。當一個線程執行到monitorenter指令時會嘗試獲取相應對象的monitor

獲取規則:

1)如果monitor的進入數爲0,則該線程可以進入monitor,並將monitor進入數設置爲1,該線程即爲monitor的擁有者

2)如果當前線程已經擁有該monitor,只是重新進入,則進入monitor的進入數加1,所以synchronized關鍵字實現的鎖是可重入鎖

3)如果monitor已被其他線程擁有,則當前線程進入阻塞狀態,直到monitor的進入數爲0,再重新嘗試獲取monitor

2.3.2.2 monitorexit

    只要擁有相應對象的monitor的線程才能執行monitorexit指令。每執行一次該指令monitor進入數減1,當進入數爲0時當前線程釋放monitor,此時其他阻塞的線程將可以嘗試獲取該monitor

3. Java對象頭

3.1 概述

    Synchronized用的鎖是存在Java對象頭裏,如果對象是數組,則虛擬機用3哥字款存儲對象頭;如果對象頭是非數組類型,則用2字款存儲對象頭

3.2 對象頭長度

1)Mark Word:默認存儲對象HashCode、分代年齡和鎖標記位

2)Class Metadata Address:存儲到對象類型數據的指針

3)Array Length:數組的長度(若當前對象是數組)

3.3 鎖

3.3.1 無鎖狀態

3.3.2 偏向鎖

1)描述:大多數情況,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要在測試一下Mark Word偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程

2)偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活着,如果不活動,則將對象頭設置成無鎖狀態;如果活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼重新偏向於其他線程,要麼恢復到無鎖活着標記對象不適合作爲偏向鎖,最後喚醒暫停的線程

3)關閉偏向鎖:在6和7中是默認啓動的,但是在應用啓動幾秒後才激活,可以使用:-XX:BiasedLockingStartupDelay=0來關閉延遲。如果希望程序裏所有鎖都處於競爭狀態,則通過-XX:UseBiasedLocking=false來關閉偏向鎖

3.3.3 輕量級鎖

1)輕量級鎖加鎖:線程在執行同步塊之前,JVM會先在當前線程的棧幀中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試自旋來獲取鎖

2)輕量級鎖解鎖:輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹稱重量級鎖。

3)總結:自旋會消耗CPU,爲了避免無用的自旋,一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他鎖嘗試獲取時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程會進行新一輪奪鎖之爭

3.3.4 重量級鎖

3.3.5 對比

1)偏向鎖

優點:加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距

缺點:如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗

使用場景:適用於只有一個線程訪問同步塊的場景

2)輕量級鎖

優點:競爭的線程不會阻塞,提高了程序的響應速度

缺點:如果始終得不到競爭的線程,自旋會消耗CPU

使用場景:追求響應時間,同步塊執行速度非常快

3)重量級鎖

優點:線程競爭不使用自旋,不會消耗CPU

缺點:線程阻塞,響應時間緩慢

使用場景:線程阻塞,響應時間緩慢

4. 原子操作的實現原理

4.1 概述

    原子操作意爲不可被中斷的一個或一系列操作

4.2 CPU術語

1)緩存行(Cache Line):緩存的最小操作單位

2)比較並交換(Compare and swap):CAS操作需要輸入兩個數值,一箇舊值和一個新值,在操作期間現比較舊的有沒有變化,如果沒有發生變化,才交換成新的,發生變化則不交換

3)CPU流水線(CPU pipeline):CPU流水線的工作方式就像工業生產上的裝配流水線,在CPU中由5~6個不同功能的電路單元組成一條指令處理流水線,然後將一條X86指令分成5~6不厚在執行

4)內存順序衝突(Memory order violation):內存順序衝突一半是由假共享引起的,假共享是指多個CPU同時修改同一個緩存行的不同部分而引起其中一個CPU的操作無效,當出現這個內存順序衝突時,CPU必須清空流水線

4.3 處理器實現原子操作
1)描述
      a) 32位IA-32處理器使用基於對緩存加鎖或總線加鎖的方式來實現多處理器之間的原子操作。
      b) 處理器保證從系統內存中讀取或寫入一個字節是原子的,即當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址
2) 機制
    a) 使用總線鎖保證原子性
         (1) 案例
                多個處理器同時對共享變量進行i++,期望結果是3,但可能是2
         (2) 原因

               多個處理器同時從各自的緩存中讀取變量i,分別加1操作,然後分別寫入系統內存
         (3) 解決方法
                使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求被阻塞,該處理器獨佔共享內存
     b) 使用緩存鎖保證原子性
         (1) 同一時刻,只需要保證某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間的通信鎖住了,鎖定期間,其他處理器不能操作其他內存地址的數據
         (2) 總線鎖把CPU和內存之間的通信鎖住,開銷比較大。頻繁使用的內存會緩存在處理器的L1、L2和L3的高速緩存,那麼原子操作可以直接在處理器內部緩存中進行,不需要聲明總線鎖。
         (3) 緩存鎖定:指內存區域如果被緩存在處理器的緩存中,並且在Lock操作期間被鎖定,那麼它執行鎖操作會寫到內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的地址。
         (4) 特殊情況(不使用緩存鎖定)
              1) 當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行,會使用總線鎖定
              2) 處理器不支持緩存鎖定

4.4 Java實現原子操作
4.4.1 使用循環CAS實現原子操作

        JVM中的CAS操作正式利用處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是循環進行CAS操作直到成功爲止
4.4.2 CAS實現原子操作的三大問題
1)ABA問題
     (a) 描述:CAS操作值時如果沒有發生變化則更新,但如果A,變成了B,又變成了A,檢查時沒有發生變化
     (b) 解決方法:每次變量更新把版本號加1,變成了1A->2B->3A,jdk1.5開始,Atomic提供了一個類 AtomicStampReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等於預期引用,並且檢查當前標誌是否等於預期標誌,若全相等,則以原子方式更新值
2)循環時間長,開銷大
      自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。若JVM能支持處理器提供的pause指令,那會提升效率。pause指令的兩個作用:第一,他可以延遲流水線執行指令,使CPU不會消耗過多的執行資源,延遲的時間取決於具體的版本。第二,它可以避免在退出循環的時候因內存順序衝突而引起CPU流水線被清空,從而提高CPU的執行效率。
3)只能保證一個共享變量的原子操作
            當對一個共享變量執行操作時,可以使用循環CAS的方式來保證原子操作,但對多個共享變量操作時,循環CAS就無法保證操作的原子性,這時可以用鎖。
4.4.3 使用鎖機制實現原子操作
        鎖機制保證了只有獲得鎖的線程才能保證鎖定的內存區域。JVM內部實現了很多中鎖機制,有偏向鎖、輕量級鎖和互斥鎖,除了循環鎖,實現鎖的方式都用了循環CAS。

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