1. 程式人生 > 實用技巧 >volatile綜合相關需要學習知識點

volatile綜合相關需要學習知識點

volatile記憶體定義

當寫一個volatile變數時,JMM會將該執行緒寫入到工作記憶體的值同步重新整理到主記憶體中。
當讀一個volatile變數時,JMM會將該執行緒工作記憶體中的值置為無效,直接從主記憶體中讀取值並重新整理到工作記憶體中。

特點

  • 只能用來修飾變數
  • 可以保證變數的可見性 ,也就是當這個變數修改後,所有執行緒都會被要求重新從主記憶體中再次拷貝一次該變數的值到工作空間中。主要是通過兩個操作來保證共享變數的可見性:
    1、被volatile關鍵詞修飾的變數,當CPU處理完該資料將資料刷回快取記憶體區時,會立馬將資料刷回記憶體。
    2、當被volatile關鍵詞修飾的變數刷回記憶體時,立馬讓其他CPU中使用的該變數無效,在最開始時是採用在總線上新增LOCK#鎖來做的,但該方式效率比較低,後續Intel將實現方式改為只要CPU在修改了被volatile修飾的共享變數後,就會向其他CPU發出訊號,將讀取到工作記憶體中的變數無效化,而其他CPU在需要使用這個變數時,如果檢測到這個無效訊號,就會重新從主記憶體中再次讀取一次該變數的值,從而保證了可見性。
  • 同時該關鍵詞還可以禁止指令重排,實現原理一個是在位元組碼層面添加了ACC_VOLATILE關鍵詞保證,另一個是在JVM中通過插入記憶體屏障來保證,記憶體屏障就是保證前者執行完之後才執行後者,有點類似依賴關係,有LOADLOAD、LOADSTORE、STORELOAD、STORESTORE四種情況。
    LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2, 在Load2及後續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢。 
    
    StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2, 在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。 
    
     LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2, 在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。
    
    StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2, 在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。
    
  • volatile無法保證原子性,是因為可能存在同時多個執行緒都要將工作記憶體的值寫入主記憶體,這時候即使CPU接收到了變數無效的訊號,也無法停止寫入的動作,因此volatile只能在CPU讀取值的時候重新重新整理變數的真實值,寫入不受限制。

使用場景

  • 使用了volatile的地方有很多,典型的是Atomic*系列裡面的value都使用了這個關鍵詞來修飾,目的就是為了保證在不同執行緒中該變數的可見性。
  • volatile適合用在一個執行緒寫,多個執行緒讀的場景,可以理解是synchronize的輕量級鎖。

相關名詞

TPS(Transactions Per Second):每秒事務處理數,衡量一個服務效能好壞的評判標準。

JMM(Java Memory Model):Java記憶體模型。

1.硬體上解決資料一致性

由於CPU有快取記憶體機制,所以在程式執行時,會將需運算的資料從主存中複製一份到快取記憶體中,在CPU進行計算時,直接在快取中進行資料的讀取和寫入,在運算完成後,再將快取中的資料重新整理到主存中。這種模式在單執行緒中是沒有任何問題的,但在多執行緒中會出現資料不一致的問題,如i++問題。對變數的操作涉及三個步驟:讀取值、操作值和重新寫入新值,這就是在多執行緒出現數據不一致的原因。

解決方式:

①在總線上加LOCK鎖的方式

在運算時,直接在總線上加鎖,可以避免快取不一致問題,但是CPU利用率極低。

②快取一致協議,參考:https://www.cnblogs.com/yjf512/p/5166415.html

2.JMM記憶體模型

JMM:用來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果。

JMM主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數在主存和從記憶體中如何取出的規則。此處的變數包括例項欄位、靜態欄位和構成陣列物件的元素,不包括區域性變數和方法引數(執行緒私有)。

JMM中規定變數都存在於主存中,執行緒都有自己的工作記憶體,執行緒對變數(是對主存中變數的副本拷貝)的操作必須在工作記憶體中,而不能直接對主存中資料進行操作,並且每個執行緒不能訪問其他執行緒的工作記憶體,執行緒間變數值的傳遞需要通過主記憶體來完成。也就是說執行緒在工作記憶體中進行資料的操作,然後再更新主存中的值。因此在多執行緒中可能出現i++問題。

對於主記憶體與工作記憶體中變數之間的互動,JMM中定義了8種操作來完成,這8種操作具有原子性。

lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒的獨佔狀態。

unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。

read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load操作使用。(工作在主存,傳輸資料到從存)

load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。

use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將執行該操作。

assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行該操作。

store(儲存):作用於工作內存的變數,它把工作記憶體中的一個變數的值傳遞到主記憶體中,以便隨後的write操作使用。(工作在從存,傳輸資料到主存)

write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數值放入主記憶體的變數中(更新操作)。

注:從主記憶體到工作記憶體,需順序執行readload,從工作記憶體到主記憶體,需順序執行storewirte操作,JMM只要求這些操作是順序執行的,並不保證連續執行。

要保證併發的正確執行,就需要保原子性、可見性和有序性。只要有一個條件未滿足,就可能導致程式執行結果不正確。

在JMM中是如何保證併發執行的正確性的,有如下三點:

①JMM只保證基本的讀取和賦值是原子性操作,如i=10;如果需要更大範圍的原子性,則需要synchronized和Lock的幫助了。

②可見性:由volatile關鍵字提供,當然通過synchronized和Lock也可以實現。

③有序性:JMM中具備一些先天的“有序性”(happens-before原則),通過volatile也可保證一定的有序性,當然通過synchronized和Lock也可保證有序性

3.volatile關鍵字

被volatile修飾的變數,具有兩種特性:

①保證變數對所有執行緒是可見的,當一條執行緒修改了變數的值時,新值對於其他執行緒來說是可以立即得知的。

對變數進行修改涉及兩個操作:a.修改執行緒工作記憶體中的值;b.將修改後的值更新到主記憶體。

當A執行緒對volatile變數的值修改時,會導致其他執行緒中的變數快取無效,所以其他執行緒再次讀取volatile變數的值時,會從主存中獲取。

禁止指令重排序優化

指令重排序優化是指處理器為了提高程式執行的效率,可能會對輸入的程式碼進行優化,它不保證程式碼的執行順序與書寫順序一致,但保證程式的最終執行結果和書寫順序的結果一致。指令

volatile能在一定程度上保證有序性,為什麼說一定程度上看下面解釋。

禁止指令重排序優化有兩層意思:

a.當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行。

從該點可以看出volatile只能在一定程度上保證有序性:A->volatile->B,整體上按照這種順序執行,但是在A操作裡還是可能存在指令重排序,只要其操作對後續操作無影響。

b.在進行指令優化時,不能將volatile變數的語句放在volatile位置之前執行,也不能把volatile變數語句放到volatile語句之後執行。可能這句話有點難以理解,通過下面虛擬碼簡單說明。

1 //a、b為非volatile變數
2 //flag為volatile變數
3  
4 a = 1;         //語句1
5 b = 2;         //語句2
6 flag = true;   //語句3
7 a = 3;         //語句4
8 b = 4;         //語句5

因為flag為volatile變數,所以執行指令重排序優化時,不能將語句3放在在語句1、2的前面;也不能將語句3放在語句4、5的後面執行。並且執行到語句3時,語句1、2已經執行完畢,並且其結果對語句3、4、5是可見的。注意語句1、2和語句4、5的順序不做任何保證(這裡也說明了volatile只能在一定程度上保證有序性)。

volatile的兩種特性,主要由記憶體屏障提供(volatile原理和實現機制)。

加入volatile關鍵字時,會多出一個lock字首指令(反編譯),該lock字首指令實際上相當於一個記憶體屏障,記憶體屏障會提供3個功能:

1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成。

2)它會強制將對快取的修改操作立即寫入主存。

3)如果是寫操作,它會導致其他CPU中對應的快取行無效。

volatile不能保證原子性,其原因可通過i++操作來分析:

i初始值為0,並且被volatile修飾,當執行緒A讀取i後,還沒對i進行自增操作時被阻塞,此時執行緒B進來,雖然volatile保證了執行緒B是從主存中讀取的資料,但是由於執行緒A並未對i值進行修改,所以執行緒B不會發現修改後的i值,這就是volatile不保證原子性的根源。

volatile的使用場景:

①對變數的寫操作不依賴於當前值。如i++場景,就不能使用volatile。

②該變數沒有包含在具有其他變數的不變式中。通俗理解:就是volatile變數不參加與其他變數的共同計算。