Valotile關鍵字詳解
在瞭解valotile關鍵字之前。我們先來了解其他相關概念。
1.1 java記憶體模型:
不同的平臺,記憶體模型是不一樣的,我們可以把記憶體模型理解為在特定操作協議下,對特定的記憶體或快取記憶體進行讀寫訪問的過程抽象。Java 虛擬機器規範中試圖定義一種 Java 記憶體模型(Java Memory Model,簡稱 JMM)來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓 Java 程式在各種平臺下都能達到一致的記憶體訪問效果,不必因為不同平臺上的物理機的記憶體模型的差異,對各平臺定製化開發程式。
更具體一點說,Java 記憶體模型提出目標在於,定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。
1.2 Java 記憶體模型的組成
主記憶體
Java 記憶體模型規定了所有變數都儲存在主記憶體(Main Memory)中(此處的主記憶體與介紹物理硬體的主記憶體名字一樣,兩者可以互相類比,但此處僅是虛擬機器記憶體的一部分)。
工作記憶體
每條執行緒都有自己的工作記憶體(Working Memory,又稱本地記憶體.),執行緒的工作記憶體中儲存了該執行緒使用到的變數的主記憶體中的共享變數的副本拷貝。
(工作記憶體是 JMM 的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化。)
JAVA記憶體模型抽象示意圖:
執行緒執行的時候,將首先從主記憶體讀值,再load到工作記憶體中的副本中,然後傳給處理器執行,執行完畢後再給工作記憶體中的副本賦值,隨後工作記憶體再把值傳回給主存,主存中的值才更新。
在這個過程中如果出現多個執行緒同時在處理這些值,豈不是會出現併發問題?
1.3 Java 記憶體間的互動操作
關於主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體之類的實現細節,Java 記憶體模型中定義了下面 8 種操作來完成。
虛擬機器實現時必須保證下面介紹的每種操作都是原子的,不可再分的(對於 double 和 long 型的變數來說,load、store、read、和 write 操作在某些平臺上允許有例外)。
8 種基本操作,如下圖:
lock (鎖定) , 作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。
unlock (解鎖) , 作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
read (讀取) , 作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的 load 動作使用。
load (載入) , 作用於工作記憶體的變數,它把 read 操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
use (使用) , 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時就會執行這個操作。
assign (賦值) , 作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
store (儲存) , 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後 write 操作使用。
write (寫入) , 作用於主記憶體的變數,它把 Store 操作從工作記憶體中得到的變數的值放入主記憶體的變數中。
Java 記憶體模型執行規則
記憶體互動基本操作的 3 個特性
Java 記憶體模型是圍繞著在併發過程中如何處理這 3 個特性來建立的.
原子性(Atomicity)
原子性,即一個操作或者多個操作要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
即使在多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒所幹擾。
可見性(Visibility)
可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
正如上面“互動操作流程”中所說明的一樣,JMM 是通過線上程 1 變數工作記憶體修改後將新值同步回主記憶體,執行緒 2 在變數讀取前從主記憶體重新整理變數值,這種依賴主記憶體作為傳遞媒介的方式來實現可見性。
有序性(Ordering)
在Java記憶體模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性
Valotile的作用?
1)保證可見性
valotile本意是不穩定的,易揮發的,也就是說,用它修飾的變數是可變的。
結合我們的Java記憶體模型介紹,我們可以瞭解在多執行緒環境下,執行緒會將執行緒間共享的變數儲存在本地記憶體中,而不是從記憶體中讀取,這就可能會引發不一致的問題,另一個程序可能在此執行緒執行期間改變了變數的值,而此執行緒並沒有看到變化。
而volatile修飾的成員變數在每次被執行緒訪問時,都強迫從共享記憶體中重讀該成員變數的值。而且,當成員變數發生變化時,強迫執行緒將變化值回寫到共享記憶體。這樣在任何時刻,兩個不同的執行緒總是看到某個成員變數的同一個值。
Java語言規範中指出:為了獲得最佳速度,允許執行緒儲存共享成員變數的私有拷貝,而且只當執行緒進入或者離開同步程式碼塊時才與共享成員變數的原始值對比。
也就是說,JVM會對執行緒變數的訪問進行優化,這樣當多個執行緒同時與某個物件時互動,就必須要注意到要讓執行緒及時的得到共享成員變數的變化。
而volatile關鍵字就是提示VM:對於這個成員變數不能儲存它的私有拷貝,而應直接與共享成員變數互動。
使用valotile前後示意圖:
從上圖可以直觀的看到valotile的實現可見性的原理,執行緒對變數讀取的時候,直接從主記憶體中讀,而不是從執行緒的工作記憶體。也就避免了其他執行緒操作時修改了變數的值。
2)禁止進行指令重排序
為了使得處理器內部的運算單元儘量被充分利用,提高運算效率,處理器可能會對輸入的程式碼進行排序。但是可能出現如下情況:
正常情況下,邏輯 A 執行完之後再執行邏輯 B。在處理器亂序執行優化情況下,有可能導致 flag 提前被設定為 true,導致邏輯 B 先於邏輯 A 執行。
這個時候volatile又起到了作用。
當程式執行到 volatile 變數的讀操作或者寫操作時, 在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行。
在進行指令優化時, 不能將在對 volatile 變數訪問的語句放在其後面執行,也不能把 volatile 變數後面的語句放到其前面執行。
(普通的變數僅僅會保證該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證賦值操作的順序與程式程式碼中的執行順序一致)
實現原理:
記憶體屏障會提供3個功能:
1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
2)它會強制將對快取的修改操作立即寫入主存;
3)如果是寫操作,它會導致其他CPU中對應的快取行無效。
總結:1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
2)禁止進行指令重排序。