volatile關鍵字詳解


volatile關鍵字詳解

volatile的三個特色

  1. 保證線程之間的可見性
  2. 禁止指令重排
  3. 不保證原子性

可見性

概念

可見性是多線程場景中才討論的,它表示多線程環境中,當一個線程修改了共享變量的值,其餘線程可以知道這個修改。java

爲何須要可見性

緩存一致性問題:緩存

public class Test {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();

        new Thread(() -> {
            try {
                //延時2s,確保進入while循環
                TimeUnit.SECONDS.sleep(2);
                //num自增
                mythread.increment();
                System.out.println("Thread-" + Thread.currentThread().getName() +
                        " current num value:" + mythread.num);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "test").start();

        while(mythread.num == 0){ 
            //dead
        }

        System.out.println("game over!!!");
    }
}

class Mythread{
    //不加volatile,主線程沒法得知num的值發生了改變,從而陷入死循環
    volatile int num = 0;

    public void increment(){
        ++num;
    }
}

如上述代碼,若是不加volatile,程序運行結果以下多線程

不加volatile

加上volatile關鍵字後,程序運行結果以下ide

加上volatile

解決方向:學習

  • 總線鎖:優化

    一次只有一個線程能經過總線進行通訊。(效率低,已棄用).net

  • MESI緩存一致性協議,CPU總線嗅探機制(監聽機制)線程

    有volatile修飾的共享變量在編譯器編譯後進行讀寫操做時,指令會多一個lock前綴,Lock前綴的指令在多核處理器下會引起兩件事情。對象


    (參考下面兩位大佬的博客)blog

    https://blog.csdn.net/jinjiniao1/article/details/100540277

    https://blog.csdn.net/qq_33522040/article/details/95319946

    • 每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置爲無效狀態, 當處理器對這個數據進行修改操做的時候,會從新從系統內存中吧數據讀處處理器緩存行裏。

    • 處理器使用嗅探技術保證它的內部緩存,系統內存和其餘處理器的緩存在總線上保持一致

    • 寫一個volatile變量時,JMM(java共享內存模型)會把該線程對應的本地內存中的共享變量值刷新到主內存;

    • 當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效,線程接下來從主內存中讀取共享變量。

禁止指令重排

指令重排概念

編譯器和CPU在保證最終結果不變的狀況下,對指令的執行順序進行重排序。

指令重排的問題

能夠與雙重檢驗實現單例模式聯繫起來看:

首先,一個對象的建立過程可大體分爲如下三步:

  1. 分配內存空間
  2. 執行對象構造方法,初始化對象
  3. 引用指向實例對象在堆中的地址

可是在實際執行過程當中,CPU可能會對上述步驟進行優化,進行指令重排

序1->3->2,從而致使引用指向了未初始化的對象,若是這個時候另一個線

程引用了該未初始化的對象(只執行了1->3兩步),就會產生異常。

不保證原子性

爲何沒法保證

具體例子

public class Test {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        for(int i = 0; i < 6666; ++i){
            new Thread(() -> {
                try {
                    mythread.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "test").start();
        }
        System.out.println("Thread-" + Thread.currentThread().getName() +
                " current num value:" + mythread.num);
    }
}

class Mythread{
    volatile int num = 0;

    public void increment(){
        ++num;
    }
}

上述代碼的運行結果以下圖

能夠看到,循環執行了6666次,但最後的結果爲6663,說明在程序運行過程當中出

現了重複的狀況。

解決方案

  1. 使用JUC中的Atomic類(以後會專門寫一篇學習筆記進行闡述)
  2. 使用synchronized關鍵字修飾(不推薦)

volatile保證可見性和解決指令重排的底層原理

內存屏障(內存柵欄)

組成

內存屏障分爲兩種:Load Barrier 讀屏障 和 Store Barrier 寫屏障

4種類型屏障

種類 例子 做用
LoadLoad屏障 Load1; LoadLoad; Load2 保證Load1讀取操做讀取完畢後再去執行Load2後續讀取操做
LoadStore屏障 Load1; LoadStore; Store2 保證Load1讀取操做讀取完畢後再去執行Load2後續寫入操做
StoreStore屏障 Store1; StoreStore; Store2 保證Load1的寫入對全部處理器可見後再去執行Load2後續寫入操做
StoreLoad屏障 Store1; StoreLoad; Load2 保證Load1的寫入對全部處理器可見後再去執行Load2後續讀取操做

做用

  1. 保證特定操做的執行順序

    在每一個volatile修飾的全局變量讀操做前插入LoadLoad屏障,在讀操做後插入LoadStore屏障

  2. 保證某些變量的內存可見性

    在每一個volatile修飾的全局變量寫操做前插入StoreStore屏障,在寫操做後插入StoreLoad屏障

相關文章
相關標籤/搜索