java volatile 看這一篇就夠了

前言

本篇文章將從java內存模型、字節碼角度解讀volatile,因爲jvm屏蔽了系統、硬件的差異,所以從這個角度出發更直觀、更易理解;網上不乏從多核cpu多級緩存或cpu lock指令去解讀volatile的,私以爲這種解讀方式有問題,比如單核cpu存在內存可見性問題嗎?似乎沒有答案。再者,volatile爲什麼會防止指令重排?僅僅是因爲lock指令嗎,要知道lock是結果,原因是volatile的可見性及happens-before原則。在介紹volatile之前必須瞭解java內存模型。

java內存模型

在這裏插入圖片描述

java內存分爲主內存線程本地內存(又稱爲緩存);主內存也稱爲堆內存,存儲靜態變量、實例數據、數組元素;在程序執行時,線程首先從主存copy變量到本地內存,修改完後,再將變量同步到主存。所以在多個線程共享同一塊主內存時,就存在使用過期數據問題。比如

public class TestVolatile {
    static boolean shutdown = false;
    //t1線程
    static void m1(){
        while(!shutdown){
        }
        System.out.println("shutdown...");
    }

    //t2線程
    static void m2(){
        shutdown = true;
        System.out.println("shutdown 更改了");
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            TestVolatile.m1();
        },"t1").start();
        //爲了讓t1充分的運行
        Thread.sleep(1000);
        //t2修改後,t1看不到volatile修改後的值
        new Thread(()->{
            TestVolatile.m2();
        },"t2").start();

    }
}

有兩個線程t1、t2,共享了靜態變量shutdown,它們分別從主內存copy了shutdown到本地內存,t1將shutdown更改爲true,同步到主存,t2看不到shutdown已經改了,仍然認爲shutdown爲false,所以t1線程永遠不會輸出shutdown;注:下圖中是t1修改了本地內存中的shutdown值,還未同步到主內存;
在這裏插入圖片描述
我們不禁會想,如果有辦法,在變量修改後,其他線程能立刻看到修改後的值,那就好了!很幸運,volatile就是幹這個事的!

volatile內存可見性

volatile[ˈvɒlətaɪl] 英文釋義易變的,volatile修飾的變量,修改後,對其它線程可見。那麼爲什麼volatile修飾的變量就對其它線程可見呢?將本例中的shutdown用volatile修飾,反編譯代碼javap -v TestVolatile 可看到shutdown多了一個acc_volatile描述符。

 static volatile boolean shutdown;
   descriptor: Z
   flags: ACC_STATIC, ACC_VOLATILE

查閱jvm字節碼指令可知,acc_volatile不允許變量緩存,這就是原因了!

ACC_VOLATILE		Declared volatile; cannot be cached.

基於字節碼的解釋,t1直接操作主存中的shutdown,而非本地內存,而t2在使用shutdown時,也從主存取值而不再從本地內存,所以shutdown修改對t2可見。volatile的內存可見性明白了,指令重排又是怎麼回事呢?

volatile防止指令重排

相信讀者也發現了,很多介紹volatile的博文中,只提到volatile有防止指令重排作用,但究竟爲什麼volatile能防止指令重排,卻語焉不詳
我們先說一下什麼是指令重排,比如以下代碼經過編譯器或者運行時都可能發生指令重排

//t1線程
int i = 1;
int j = 2;

變爲

//t1線程
int j = 2;
int i = 1;

這是被允許的,因爲i與j沒有依賴性,所以指令重排不會影響最終結果正確性。這在單線程環境下是沒問題的,但在多線程環境下會存在問題。比如

//t2線程
1、if(j==2){
2、 //認爲此時的i肯定等於1,從而進行一些操作,其實此處的i可能等於初始值0(假設i是成員變量)
   }

那有什麼辦法可以防止指令重排?volatile!本例中,用volatile修改j

int i = 1;
volatile int j = 2;

那麼爲什麼volatile能防止指令重排呢?這就要說說java內存模型中的happens-before原則了,happens-before規定了代碼中的執行順序和內存可見性,happens-before原則有很多條,其中有一條叫程序次序規則(program order rule),說的是在單線程裏,書寫在前面的代碼happens-before書寫在後邊的代碼。比如,actionA happens-before actionB,那麼actionA對actionB是可見的。基於此,t1裏的volatile j=2happens-before t2裏的if(j===2) ,又t1線程裏i=1 happens-before volatile j=2,所以 i=1 happens-before if(j===2),所以i==1 對t2中的第2行代碼是可見的。正因爲如此,就保證了i==1不能重排到j==2後邊,所以防止了指令重排。從上可以看出指令重排是volatile內存可見性的副作用。同理volatile後邊的語句,也不能指令重排到前邊。
這個地方要好好理解,最初t1中的i、j沒有依賴關係,所以可指令重排;但當t1中的j與t2中的j,產生了依賴後,導致了t1中的i與j也產生了依賴關係,所以i、j不能指令重排。

總結

volatile的作用

  • 保證內存可見性,變量不能緩存,可以認爲線程直接修改主存中的變量、直接從主存中讀取變量;
  • 防止指令重排序,這是由happens-before中的單線程內有序及volatile可見性衍生出的副作用

常見問題

  • 哪些數據可以共享?
    • 靜態變量、實例數據、數組元素。只要數據存在共享就會存在內存可見性問題。
  • 哪些變量不存在共享?
    • 局部變量、方法參數是線程私有,所以不存在數據共享問題
  • 怎樣理解指令重排與程序次序規則?
    • 如果認真思考一下,很容易想到,int i=1; int j=1; 指令重排序與int i=1 happens-beforeint j=1衝突吧?其實是不衝突的。happens-before 並不意味着代碼的執行順序,本例中int i=1 並一定在int j=1 之前執行,只要保證最終執行結果與happens-before的執行結果一致即可,以下引用java語言規範,在Stack Overflow也有類似提問。

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

參考

When does java thread cache refresh happens?
loop doesnt see value changed by other thread without a printstatement
how to decompile volatile variable in java
instruction reordering happens before relationship in java

9年全棧工作經驗,歡迎關注個人公衆號
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章