1. 程式人生 > 其它 >簡單聊聊volatile關鍵字原理

簡單聊聊volatile關鍵字原理

volatile 關鍵字

一、說說你對 volatile 關鍵字的理解

被 volatile 修飾的共享變數,就具有了以下兩點特性:

  • 保證了不同執行緒對該變數操作的記憶體可見性;

  • 禁止指令重排序

二、記憶體可見性 和 禁止重排序分別怎麼實現的?

當一個變數被 volatile 修飾時,那麼對它的修改會立刻重新整理到主存,當其它執行緒需要讀取該變數時,會去記憶體中重新讀取新值。

禁止重排序,內部提供了記憶體屏障,寫的指令不能往後排,讀的指令不能往前排

1.可見性(Visibility): 說到可見性,Java就是利用volatile來提供可見性的。 當一個變數被volatile修飾時,那麼對它的修改會立刻重新整理到主存,當其它執行緒需要讀取該變數時,會去記憶體中讀取新值。而普通變數則不能保證這一點。 其實通過synchronized和Lock也能夠保證可見性,執行緒在釋放鎖之前,會把共享變數值都刷回主存,但是synchronized和Lock的開銷都更大。

2. 有序性(Ordering) JMM是允許編譯器和處理器對指令重排序的,但是規定了as-if-serial語義,即不管怎麼重排序,程式的執行結果不能改變。加上volatile關鍵字,禁止重排序,可以確保程式的有序性,也可以上重量級的synchronized和Lock來保證有序性,它們能保證那一塊區域裡的程式碼都是一次性執行完畢的。

三、volatile 關鍵字如何滿足併發程式設計的三大特性的?

當寫一個 volatile 變數時,JMM 會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體,當讀一個volatile變數時,JMM 會把該執行緒對應的本地記憶體置為無效,執行緒接下來將從主記憶體中讀取共享變數。

四、volatile 的兩點記憶體語義能保證可見性和有序性,但是能保證原子性嗎?

不能保證原子性

比如下面的例子:

public class Test {
    public volatile int inc = 0;
 
    public void increase() {
        inc++;
    }
 
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)  //保證前面的執行緒都執行完
            Thread.yield();
        System.out.println(test.inc);
    }

按道理來說結果是10000,但是執行下很可能是個小於10000的值。有人可能會說volatile不是保證了可見性啊,一個執行緒對inc的修改,另外一個執行緒應該立刻看到啊!可是這裡的操作inc++是個複合操作啊,包括讀取inc的值,對其自增,然後再寫回主存。 假設執行緒A,讀取了inc的值為10,這時候被阻塞了,因為沒有對變數進行修改,觸發不了volatile規則。 執行緒B此時也讀讀inc的值,主存裡inc的值依舊為10,做自增,然後立刻就被寫回主存了,為11。 此時又輪到執行緒A執行,由於工作記憶體裡儲存的是10,所以繼續做自增,再寫回主存,11又被寫了一遍。所以雖然兩個執行緒執行了兩次 increase(),結果卻只加了一次。 有人說,volatile 不是會使快取行無效的嗎?但是這裡執行緒A讀取到執行緒B也進行操作之前,並沒有修改inc值,所以執行緒B讀取的時候,還是讀的10。 又有人說,執行緒B將11寫回主存,不會把執行緒A的快取行設為無效嗎?但是執行緒A的讀取操作已經做過了啊,只有在做讀取操作時,發現自己快取行無效,才會去讀主存的值,所以這裡執行緒A只能繼續做自增了。 綜上所述,在這種複合操作的情景下,原子性的功能是維持不了了。但是volatile在上面那種設定flag值的例子裡,由於對flag的讀/寫操作都是單步的,所以還是能保證原子性的。 要想保證原子性,只能藉助於synchronized,Lock以及併發包下的atomic的原子操作類了,即對基本資料型別的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數),減法操作(減一個數)進行了封裝,保證這些操作是原子性操作。

五、知道 volatile 底層的實現機制?

  1. 重排序時不能把後面的指令重排序到記憶體屏障之前的位置

  2. 使得本CPU的Cache寫入記憶體

  3. 寫入動作也會引起別的CPU或者別的核心無效化其Cache,相當於讓新寫入的值對別的執行緒可見。

六、 你在哪裡會使用到volatile,舉兩個例子呢?

1.狀態量標記,就如上面對flag的標記,我重新提一下:

int a = 0;
volatile bool flag = false;

public void write() {
    a = 2;              //1
    flag = true;        //2
}

public void multiply() {
    if (flag) {         //3
        int ret = a * a;//4
    }
}

這種對變數的讀寫操作,標記為volatile可以保證修改對執行緒立刻可見。比synchronized,Lock有一定的效率提升。

2.單例模式的實現,典型的雙重檢查鎖定(DCL)

class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

這是一種懶漢的單例模式,使用時才建立物件,而且為了避免初始化操作的指令重排序,給instance加上了volatile。

參考: https://www.zhangshengrong.com/p/ERNnQloQa5/