1. 程式人生 > >淺談volatile關鍵字

淺談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修飾,程式執行的順序可能會進行重排序。