變量可見性及volatile關鍵字

目標:

1.瞭解什麼是變量可見性問題

2.造成變量可見性問題原因

3.volatile關鍵字


前提說明:變量可見性問題發生在多線程,只有多線程纔有變量可見性問題

1.什麼是變量可見性問題

問題1:如何在多線程之間共享變量?使用全局變量:靜態變量或者共享對象

問題2:一個變量在線程1中被改變值了,在線程2中能看到此變量的最新值嗎?不一定

有如下代碼,執行結果是什麼呢?線程會停止循環打印出i的值嗎?

import java.util.concurrent.TimeUnit;

public class TestVolatile {
    private static boolean isRunning = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                while (isRunning){
                    i++;
                }
                System.out.println(i);
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(2); //兩秒之後
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isRunning = false;
        System.out.println("isRunning被設置爲false了");
    }
}

結果線程並沒有停止,i的值並沒有打印出來。這就是併發中的變量可見性問題。即所見非所得。

怎麼才能可見呢?有兩種方式

1.加synchronized

2.加volatile關鍵字修飾

import java.util.concurrent.TimeUnit;

public class TestVolatile {
//    private static boolean isRunning = true;
    //第二種方式
    private static volatile boolean isRunning = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                while (isRunning){
                    i++;
                    //第一種方式
//                    synchronized (this){
//                        i++;
//                    }
                }
                System.out.println(i);
            }
        }).start();
        
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isRunning = false;
        System.out.println("isRunning被設置爲false了");
    }
}

2.造成變量可見性問題原因

先了解下jmm(java內存模型)規範

1.共享變量必須放在主內存中

2.線程有自己的工作內存,線程只可操作自己的工作內存

3.線程要操作共享變量,需要從主內存讀取到工作內存中,改變值後需要從工作內存同步到主內存中。

這就是線程安全問題根因,導致了變量可見性問題以及變量原子性問題,此處先討論可見性問題。

如圖顯示了線程的流程。

問題1:變量在內存之間同步的時候被緩存了嗎?

根據java內存模型我們可以知道變量修改完會同步到工作內存中。而且2秒肯定一定把變量同步到工作內存了。而內存之間的高速緩存並不是導致此問題的原因,這是更底層的開發者纔會涉及的問題。java開發者可以不考慮各內存之間高速緩存問題。

問題2:使用共享變量前不會重新從主內存讀取值嗎?問題我們先放下。再瞭解一下指令重排

java編程語言的語義允許java編譯器和微處理器進行執行優化,這些優化導致了與其交互的代碼不再同步,從而導致看似矛盾的行爲。

如圖所示,這是一個簡單的指令重排,導致結果跟代碼執行結果不一致。

那麼java中的指令重排什麼時候會發生?。

這就不得不說到java的jit編譯器(Just In Time Compiler)。java是解釋性語言及編譯性語言的混合體。

解釋執行:即腳本,在執行時候,由語言的解釋器將其一條條翻譯成機器可識別的指令。

編譯執行:將我們編寫的程序直接編譯成機器可以識別的指令碼。即把整個批量編譯。再分析一下代碼執行。

java把java文件編譯成類似字節碼文件,並加載到jvm進程中執行

jit此時就把循環這塊進行了isRunning緩存及指令重排。如上圖所示。這就導致他並不會重新從主內存讀取isRunning的值,並且主內存中此變量值改變了也不會導致線程中的while循環停止。所以此處就導致了可見性問題。

3.volatile關鍵字

解決此問題可以使用volatile關鍵字,即上文中的代碼中的第二種方式。第一種方式加synchronized解決原因先不討論。

根據jmm中規定的happen before和同步規則,對某個volatile字段的寫操作happends-before每個後續對該volatile字段的讀操作。對volatile變量v的寫入,與所有其它線程後續對v的讀同步。所以volatile關鍵字滿足一下功能:

1.禁止緩存;(volatile變量的訪問控制符會加個ACC_VOLATILE,此處可以參考oracle官網相應描述)

2.對volatile變量相關的指令不做重排序。

到此時我們就基本明白了變量可見性問題的原因及解決方案。

 

之後會繼續寫一下java中的原子操作以及java變量的原子性問題。

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