1. 程式人生 > >JAVA多執行緒雜學4-2018年10月28日

JAVA多執行緒雜學4-2018年10月28日

volatile的應用

在多執行緒併發程式設計中synchronized和volatile都扮演著重要的角色,volatile是輕量級的synchronized,它在多處理器開發中保證了共享變數的“可見性”。可見性的意思是當一個執行緒修改一個共享變數時,另外一個執行緒能讀到這個修改的值。

注意點

①確保某一執行緒修改後所有執行緒能用修改後的結果去操作

②修飾變數

術語

①記憶體屏障:是一組CPU處理器指令,用於實現對記憶體操作順序的限制。(說白了就是CPU操作記憶體順序)

②緩衝行:快取中可以分配的最小儲存單位。CPU處理器填寫緩衝線時會載入整個緩衝線,需要使用多個主記憶體讀週期。(快取最小儲存單位)

③原子操作:不可中斷的一個或者一系列操作。

④快取行填充:當CPU處理器識別到從記憶體中讀取運算元是可以快取時,CPU處理器讀取整個快取行到適當的快取(L1/2/3或者所有)(當運算元可以快取到CPU快取記憶體時,則一次性讀取並快取整個快取行)

⑤快取命中:如果進行高速緩衝行填充操作的記憶體位置仍然是下次CPU處理器訪問的地址時,CPU處理器從CPU快取記憶體中讀取運算元而不是從記憶體讀取。(CPU從CPU快取記憶體讀取而不是從記憶體讀取的行為)

⑥寫命中:當CPU處理器將運算元寫回到一個記憶體緩衝區域時,CPU首先會檢查這個快取的記憶體地址是否在快取行中,如果存在一個有效的快取行則CPU處理器將這個運算元寫回到快取而不是寫回到記憶體(CPU寫入CPU快取記憶體而不是寫入記憶體的行為)

⑦寫缺失:一個有效快取行被寫入到不存在的記憶體區域(無法從CPU快取記憶體回寫到記憶體的行為)

整體流程

為了提高處理速度,處理器不直接和記憶體進行通訊,而是先將系統記憶體的資料讀到內部快取(L1,L2或其他)後再進行操作,但操作完不知道何時會寫到記憶體。如果對聲明瞭volatile的變數進行寫操作,JVM就會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫回到系統記憶體。但是,就算寫回到記憶體,如果其他處理器快取的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的快取是一致的,就會實現快取一致性協議,每個處理器通過嗅探在總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取裡。

Synchonized

synchronized用的鎖是存在Java物件頭裡的。32位JVM中JAVA物件頭中的Mark Word結構如下圖:

64位JVM中JAVA物件頭中的Mark Word結構如下圖:

32位JVM在執行期間,Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。Mark Word可能變化為儲存以下4種資料:

Java SE 1.6為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率

偏向鎖

經過研究發現,很多情況下鎖不僅不存在多執行緒競爭關係,即便存在也總是同一執行緒獲此獲得,為了減輕這種情況下的獲得鎖/釋放鎖的代價而發明了偏向鎖。

當一個執行緒訪問同步塊並獲取到鎖後,會在物件頭和棧幀中的鎖記錄裡記錄當前執行緒ID,以後該執行緒再進入和退出同步塊時只要測試物件頭的Mark Word裡是否儲存著指向當前執行緒ID的偏向鎖,這樣就不需要進行CAS操作來加鎖和解鎖,

如果記錄著當前執行緒ID則表示執行緒之前已經獲得了鎖。如果不是當前執行緒ID則需要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):如果沒有設定則使用CAS競爭鎖;如果設定了則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒ID。

撤銷偏向鎖

釋放原則就是一旦出現競爭則釋放鎖,不出現競爭則不釋放鎖(預設不存在競爭關係,既然存在那就立即釋放鎖)。偏向鎖撤銷的流程是這樣的:首先暫停持有偏向鎖的執行緒,然後檢視其是否活著:如果其不處於活動狀態,則將物件頭設定成無鎖狀態;如果其仍然活著,則遍歷偏向物件的鎖記錄,持有偏向鎖的棧中鎖記錄和JAVA物件頭的Mark Word要麼重新變為其他執行緒ID、要麼恢復到無鎖狀態或者標記物件不適合作為偏向鎖,最後喚醒第一步中被暫停的執行緒。

關閉偏向鎖

偏向鎖在Java 6和Java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才啟用,如有必要可以使用JVM引數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應用程式裡所有的鎖通常情況下處於競爭狀態,可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程式預設會進入輕量級鎖狀態。

輕量級鎖

輕量級鎖加鎖

執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標(存放地址)。如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖

輕量級鎖解鎖

搶奪鎖失敗的執行緒會通過自旋來再次獲取鎖,但這不意味其會無限自旋,一方面自旋會消耗CPU、另一方面已經獲得鎖的執行緒可能被阻塞住進而造成無用的自旋,所以,鎖會從輕量級升級到重量級鎖。一旦升級成重量級鎖之後,所有其他執行緒試圖獲取鎖時都會被阻塞,

當持有鎖的執行緒解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到物件頭,即把JAVA物件中的Mark Word所儲存的指標替換為指標所指向的值,如果成功則表示沒有競爭發生;如果失敗則表示當前鎖存在競爭,鎖已經升級為重量級鎖。此時此刻持有鎖的執行緒釋放鎖之後會喚醒所有被阻塞的執行緒,被喚醒的執行緒就會進行新一輪的奪鎖之爭。

鎖的優缺點對比

偏向鎖

優點:加鎖和解鎖不需要額外的消耗(以為壓根就不需要加解鎖,就是替換個數值而已),和執行非同步方法相比僅存在納秒級別的差距。

缺點:如果執行緒之間存在鎖競爭的話,會帶來額外的鎖撤銷的消耗,畢竟一旦出現競爭關係,偏向鎖就得收回,影響已經獲取到鎖的執行緒(因為首先會暫停已經獲取到鎖的執行緒)。

適用場景:適用於只有一個執行緒訪問同步程式碼塊的場景。

輕量級鎖

優點:競爭的執行緒不會阻塞已經獲取到鎖的執行緒(偏向鎖會),進而提高了程式的響應速度。

缺點:如果始終得不到鎖的競爭執行緒,使用自旋會消耗CPU。

適用場景:追求響應時間,同步程式碼塊執行速度非常快(通過JVM引數直接關閉偏向鎖,直接從輕量級鎖開始)

重量級鎖

優點:執行緒競爭不使用自旋,因此不會消耗CPU。

缺點:競爭同一個鎖的其他執行緒都被阻塞,響應時間緩慢。

適用場景:追求吞吐量,同步程式碼塊執行速度較長。