天天看點

volatile原理總結(轉載)

記憶體可見性

記憶體可見性相關概念:線程對共享變量修改的可見性。當一個線程修改了共享變量的值,其他線程能夠立刻得知這個修改。 後面會繼續總結一篇《Java記憶體模型(JMM)總結》以較長的描述記憶體可見性的概念。

volatile使用Lock字首的指令禁止線程本地記憶體緩存,保證不同線程之間的記憶體可見性。

Java代碼如下:

Singleton volatile instance = new Singleton();                // instance是volatile變量

轉變成彙編代碼,如下:

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

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

Lock字首的指令在多核處理器下會引發了兩件事情:

1)将目前處理器緩存行的資料寫回到系統記憶體。

2)一個處理器的緩存回寫到記憶體會導緻其他處理器的緩存無效。在多核處理器系統中進行操作的時候,IA-32和Intel 64處理器能嗅探其他處理器通路系統記憶體和它們的内部緩存,處理器使用嗅探技術保證它的内部緩存、系統記憶體和其他處理器的緩存的資料在總線上保持一緻

了解volatile特性的一個好方法是把對volatile變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步。從記憶體語義的角度來說,volatile的寫-讀與鎖的釋放-擷取有相同的記憶體效果:volatile寫和鎖的釋放有相同的記憶體語義;volatile讀與鎖的擷取有相同的記憶體語義——這使得volatile變量的寫-讀可以實作線程之間的通信。

volatile的記憶體語義:

volatile寫的記憶體語義:當寫一個volatile變量時,JMM會把該線程對應的本地記憶體中的共享變量值重新整理到主記憶體

volatile讀的記憶體語義:當讀一個volatile變量時,JMM會把該線程對應的本地記憶體置為無效。線程接下來将從主記憶體中讀取共享變量。

volatile寫 - 讀的記憶體語義:

線程A寫一個volatile變量,實質上是線程A向接下來将要讀這個volatile變量的某個線程發出了(其對共享變量所做修改的)消息。 

線程B讀一個volatile變量,實質上是線程B接收了之前某個線程發出的(在寫這個volatile變量之前對共享變量所做修改的)消息。 

線程A寫一個volatile變量,随後線程B讀這個volatile變量,這個過程實質上是線程A通過主記憶體向線程B發送消息。

volatile原理總結(轉載)

volatile關鍵字本身就包含了禁止指令重排序的語義。

指令重排序對記憶體可見性的影響:

volatile原理總結(轉載)

當1和2之間沒有資料依賴關系時,1和2之間就可能被重排序(3和4類似)。這樣的結果就是:讀線程B執行4時,不一定能看到寫線程A在執行1時對共享變量的修改。

volatile禁止指令重排序語義的實作:

記憶體屏障:

重排序可能會導緻多線程程式出現記憶體可見性問題。對于處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的記憶體屏障(Memory Barriers,Intel稱之為Memory Fence)指令,通過記憶體屏障指令來禁止特定類型的處理器重排序。通過禁止特定類型的編譯器重排序和處理器重排序,為程式員提供一緻的記憶體可見性保證。

為了保證記憶體可見性,Java編譯器在生成指令序列的适當位置會插入記憶體屏障指令來禁止特定類型的處理器重排序。

volatile原理總結(轉載)

StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他3個屏障的效果。現代的多處理器大多支援該屏障(其他類型的屏障不一定被所有處理器支援)。執行該屏障開銷會很昂貴,因為目前處理器通常要把寫緩沖區中的資料全部重新整理到記憶體中(Buffer Fully Flush)。

JMM針對編譯器制定volatile重排序規則表:

volatile原理總結(轉載)

當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確定volatile寫之前的操作不會被編譯器重排序到volatile寫之後。 

當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確定volatile讀之後的操作不會被編譯器重排序到volatile讀之前。 

當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序

為了實作volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序。

下面是基于保守政策的JMM記憶體屏障插入政策:

在每個volatile寫操作的前面插入一個StoreStore屏障。 

在每個volatile寫操作的後面插入一個StoreLoad屏障。 

在每個volatile讀操作的後面插入一個LoadLoad屏障。 

在每個volatile讀操作的後面插入一個LoadStore屏障。

從編譯器重排序規則和處理器記憶體屏障插入政策來看,隻要volatile變量與普通變量之間的重排序可能會破壞volatile的記憶體語義(記憶體可見性),這種重排序就會被編譯器重排序規則和處理器記憶體屏障插入政策禁止。

對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種複合操作不具有原子性,因為本質上volatile++是讀、寫兩次操作。

對于複合操作,可以:

同步塊技術(鎖)

Java concurrent包(原子操作類等)

總結

volatile特點:

通過使用Lock字首的指令禁止變量線上程工作記憶體中緩存來保證volatile變量的記憶體可見性、通過插入記憶體屏障禁止會影響變量記憶體可見性的指令重排序

對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種複合操作不具有原子性