Java併發編程 - volatile 怎麼保障內存可見性 & 防止指令重排序?

內存可見性

首先,要明確一下這個內存的含義,內存包括共享主存和高速緩存(工作內存),Volatile關鍵字標識的變量,是指CPU從緩存讀取數據時,要判斷數據是否有效,如果緩存沒有數據,則再從主存讀取,主存就不存在是否有效的說法了。而內存一致性協議也是針對緩存的協議。

內存可見性意思是一個CPU核心對數據的修改,對其他CPU核心立即可見,這句話拆開了理解:

1、CPU修改數據,首先是對工作內存的修改,也有人說被volatile修飾的變量不會拷貝副本到工作內存,而是直接修改主存,我覺得這個說法是不對的,CPU對數據的修改總是先修改工作內存,然後再同步回主內存,只不過是對被volatile修飾變量的修改,會立刻同步回主內存,假如只有一個線程修改volatile變量,那麼這個變量在工作內存的副本會一直有效,CPU也不會每次修改都從主存讀取volatile變量,只是每次修改後都會及時更新主存罷了。

2、對其他核心立即可見,這個的意思是,當一個CPU核心A修改完volatile變量,並且立即同步回主存,如果CPU核心B的工作內存中也緩存了這個變量,那麼B的這個變量將立即失效,當B想要修改這個變量的時候,B必須從主存重新獲取變量的值。

說了這麼多,volatile有什麼用呢?哎,這個作用一定要說清楚,不然很容易忘記!

舉個例子:

public class VolatileTest implements Runnable {
 
    static boolean flag = true;
 
    @Override
    public void run() {
        while (flag) {
        }
        System.out.println("end......");
    }
 
    public static void main(String[] args) {
        new Thread(new VolatileTest()).start();
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;
        System.out.println("end main......");
    }
}

// 輸出
end main......

上面這個例子,子線程會一直卡住,原因就是flag不具備可見性,主線程和子線程剛開始都緩存了flag,且值是true,後來主線程把flag改成了false,但是子線程並不知道,僅此而已,僅此而已!!!!如果把flag用volatile修改,那麼主線修改成false後,子線程再次while循環的時候,就會發現它緩存的flag已經失效了,它會去主存重新讀取flag的值。

實現的原理一般都是基於CPU的MESI協議(緩存一致性協議),其中E表示獨佔Exclusive,S表示Shared,M表示Modify,I表示Invalid,如果一個核心修改了數據,那麼這個核心的數據狀態就會更新成M,同時其他核心上的數據狀態更新成I,這個是通過CPU多核之間的嗅探機制實現的。

但是,這樣是否就能保證多線程操作一個共享變量的時候,保證線程安全呢?其實不然,否則我怎麼說是僅此而已呢!

volatile限定的是從緩存讀取時刻的校驗,如果兩個CPU同時從各自緩存讀取一個變量n=1(此時,變量n在各個CPU緩存上都是有效的),並且同時修改了變量n=n+1,再寫回緩存,這個時候n的值等於2,而不是等於3。因此,在多線程操作共享變量(例如:計數器)的時候,正確的方式是使用同步或者Atomic工具類。

 

指令有序性

這個涉及到內存屏障(Memory Barrier),內存屏障有兩個能力:

a、就像一套柵欄分割前後的代碼,阻止柵欄前後的沒有數據依賴性的代碼進行指令重排序,保證程序在一定程度上的有序性。
b、強制把寫緩衝區/高速緩存中的髒數據等寫回主內存,讓緩存中相應的數據失效,保證數據的可見性。

首先,指令並不是代碼行,指令是原子的,通過javap命令可以看到一行代碼編譯出來的指令,當然,像int i=1;這樣的代碼行也是原子操作。

在單例模式中,Instance inst = new Instance();   這一句,就不是原子操作,它可以分成三步原子指令:

  1. 分配內存地址
  2. new一個Instance對象
  3. 將內存地址賦值給inst

CPU爲了提高執行效率,這三步操作的順序可以是123,也可以是132,如果是132順序的話,當把內存地址賦給inst後,inst指向的內存地址上面還沒有new出來單例對象,這時候,如果就拿到inst的話,它其實就是空的,會報空指針異常。這就是爲什麼雙重檢查單例模式中,單例對象要加上volatile關鍵字。

 

內存屏障有三種類型和一種僞類型

a、lfence:即讀屏障(Load Barrier),在讀指令前插入讀屏障,可以讓高速緩存中的數據失效,重新從主內存加載數據,以保證讀取的是最新的數據。

b、sfence:即寫屏障(Store Barrier),在寫指令之後插入寫屏障,能讓寫入緩存的最新數據寫回到主內存,以保證寫入的數據立刻對其他線程可見。

c、mfence,即全能屏障,具備ifence和sfence的能力。

d、Lock前綴:Lock不是一種內存屏障,但是它能完成類似全能型內存屏障的功能。

 

併發三特性總結

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