淺談volatile關鍵字
Java的volatile關鍵字在JDK原始碼中經常出現,但是對它的認識只是停留在共享變數上,今天來談談volatile關鍵字。
volatile,從字面上說是易變的、不穩定的,事實上,也確實如此,這個關鍵字的作用就是告訴編譯器,只要是被此關鍵字修飾的變數都是易變的、不穩定的。那為什麼是易變的呢?因為volatile所修飾的變數是直接存在於主記憶體中的,執行緒對變數的操作也是直接反映在主記憶體中,所以說其是易變的。
什麼是主記憶體?為什麼是在主記憶體中?先看看java的記憶體模型(JMM)中記憶體與執行緒的關係。
圖片來自《深入理解Java虛擬機器》
JMM中的記憶體分為主記憶體和工作記憶體,其中主記憶體是所有執行緒共享的,而工作記憶體是每個執行緒獨立分配的,各個執行緒的工作記憶體之間相互獨立、互不可見。線上程啟動的時候,虛擬機器為每個記憶體分配了一塊工作記憶體,不僅包含了執行緒內部定義的區域性變數,也包含了執行緒所需要的共享變數的副本,當然這是為了提高執行效率,讀副本的比直接讀主記憶體更快。
那麼對於volatile修飾的變數(共享變數)來說,在工作記憶體發生了變化後,必須要馬上寫到主記憶體中,而執行緒讀取到是volatile修飾的變數時,必須去主記憶體中去獲取最新的值,而不是讀工作記憶體中主記憶體的副本,這就有效的保證了執行緒之間變數的可見性。
volatile特性一:記憶體可見性,即執行緒A對volatile變數的修改,其他執行緒獲取的volatile變數都是最新的。
舉個栗子:
volatile boolean flag;
...
while(!flag){
doSomeThing();
}
檢查標記判斷退出迴圈
volatile的例子很難重現,因為只有在對變數讀取頻率很高的情況下,虛擬機器才不會及時寫回到主記憶體,而當頻率沒有達到虛擬機器認為的高頻率時,普通變數和volatile是同樣的處理邏輯。
volatile特性二:可以禁止指令重排序
至於重排序是啥?我們通過個簡單的例子瞭解下。
public class SimpleHappenBefore {
/** 這是一個驗證結果的變數 */
private static int a=0;
/** 這是一個標誌位 */
private static boolean flag=false;
public static void main(String[] args) throws InterruptedException {
//由於多執行緒情況下未必會試出重排序的結論,所以多試一些次
for (int i=0;i<1000;i++){
ThreadA threadA=new ThreadA();
ThreadB threadB=new ThreadB();
threadA.start();
threadB.start();
//這裡等待執行緒結束後,重置共享變數,以使驗證結果的工作變得簡單些.
threadA.join();
threadB.join();
a=0;
flag=false;
}
}
static class ThreadA extends Thread{
public void run(){
a=1;
flag=true;
}
}
static class ThreadB extends Thread{
public void run(){
if(flag){
a=a*1;
}
if(a==0){
System.out.println("ha,a==0");
}
}
}
}
這裡有兩個共享變數a和flag,初始值分別為0和false。在ThreadA中先給a=1,然後flag=true。 如果按照有序的話,那麼在ThreadB中如果if(flag)成功的話,則應該a=1,而a=a*1之後a仍然為1,下方的if(a==0)應該永遠不會為真,永遠不會列印。
但實際情況是,在試驗100次的情況下會出現0次或幾次的列印結果,而試驗1000次結果更明顯,有十幾次列印。
以上這種現象就是由於指令重排序造成的。
那麼什麼是指令重排序?–為了儘可能減少記憶體操作速度遠慢於CPU執行速度所帶來的CPU空置的影響,虛擬機器會按照自己的一些規則將程式編寫順序打亂。
如果變數沒有volatile修飾,程式執行的順序可能會進行重排序。