1. 程式人生 > 實用技巧 >理解Volatile關鍵字,其實看這一篇就夠了,寫的非常細緻

理解Volatile關鍵字,其實看這一篇就夠了,寫的非常細緻

前言

volatile是Java虛擬機器提供的輕量級的同步機制。

volatile關鍵字作用是什麼?

兩個作用:

1.保證被volatile修飾的共享變數對所有執行緒總數可見的,也就是當一個執行緒修改了一個被volatile修飾共享變數的值,新值總是可以被其他執行緒立即得知。

2.禁止指令重排序優化。

volatile的可見性

關於volatile的可見性作用,我們必須意識到被volatile修飾的變數對所有執行緒總數立即可見的,對volatile變數的所有寫操作總是能立刻反應到其他執行緒中;

下面來測試一下,此時的還未initFlag被volatile修飾。

private boolean initFlag = false;
 
public void test() throws InterruptedException{
    Thread threadA = new Thread(() -> {
        while (!initFlag) {
 
        }
        String threadName = Thread.currentThread().getName();
        System.out.println("執行緒" + threadName+"獲取到了initFlag改變後的值");
    }, "threadA");
 
    //執行緒B更新全域性變數initFlag的值
    Thread threadB = new Thread(() -> {
        initFlag = true;
    }, "threadB");
    
    //確保執行緒A先執行
    threadA.start();
    Thread.sleep(2000);
    threadB.start();
}

執行結果:控制檯只打印了 "執行緒threadB改變了initFlag的值",且程式並未終止。

此時initFlag已經被volatile關鍵字修飾了

private volatile boolean initFlag = false;
 
public void test() throws InterruptedException{
    Thread threadA = new Thread(() -> {
        while (!initFlag) {
 
        }
        String threadName = Thread.currentThread().getName();
        System.out.println("執行緒" + threadName+"獲取到了initFlag改變後的值");
    }, "threadA");
 
    Thread threadB = new Thread(() -> {
        initFlag = true;
        String threadName = Thread.currentThread().getName();
        System.out.println("執行緒" + threadName+"改變了initFlag的值");
    }, "threadB");
 
    //確保執行緒A先執行
    threadA.start();
    Thread.sleep(2000);
    threadB.start();
}

執行結果:

執行緒threadB改變了initFlag的值
執行緒threadA獲取到了initFlag改變後的值

並且程式已經結束了。

這個案例充分說明了volatile的可見性作用。

volatile無法保證原子性

來個案例說明一切:

private static volatile int count = 0;
/**
 * count雖然被volatile關鍵字修飾,但是結果並不是50000,而是小於等於50000
 **/
public static void main(String[] args) throws InterruptedException{
 
    //開啟10個執行緒,分別對count進行自增操作
    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                count++;    //先讀,再加,不是一個原子操作
            }
        });
        thread.start();
    }
    Thread.sleep(2000);
    
    System.out.println("count==" + count);
}

count雖然被volatile關鍵字修飾了,但是輸出的結果會小於等於50000,足以說明了volatile無法保證原子性。

volatile禁止重排優化

volatile關鍵字另一個作用就是禁止指令重排優化,從而避免多執行緒環境下程式出現亂序執行的現象。

記憶體屏障,又稱記憶體柵欄,是一個CPU指令,它的作用有兩個,一是保證特定操作的執行順序,二是保證某些變數的記憶體可見性(利用該特性實現volatile的記憶體可見性)。由於編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴

編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排序,也就是說通過插入記憶體屏障禁止在記憶體屏障前後的指令執行重排序優化。Memory Barrier的另外一個作用是強制刷出各種CPU的快取資料,因此任何CPU上的執行緒都能讀取到這些資料的最新版本。

總之,volatile變數正是通過記憶體屏障實現其在記憶體中的語義,即可見性和禁止重排優化。

下面看一個非常典型的禁止重排優化的例子,如下:

//禁止指令重排優化
private volatile static VolatileSingleton singleton;
 
public static VolatileSingleton getInstance(){
    if(singleton != null){
        synchronized (VolatileSingleton.class){
            if(singleton != null){
                //多執行緒環境下可能會出現問題的地方
                singleton = new VolatileSingleton();
            }
        }
    }
    return singleton;
}

新new一個物件是分為三步來完成:

memory = allocate();//1.分配物件記憶體空間

instance(memory);//2.初始化物件

singleton = memory;//3.設定singleton物件指向剛分配的記憶體地址,此時singleton != null

由於步驟1和步驟2間可能會重排序,如下:

memory = allocate();//1.分配物件記憶體空間

singleton = memory;//3.設定singleton物件指向剛分配的記憶體地址,此時singleton != null

instance(memory);//2.初始化物件

由於步驟2和步驟3不存在資料依賴關係,而且無論重排前還是重排後程序的執行結果,在單執行緒中並沒有改變,因此這種重排優化是允許的。但是指令重排只會保證序列語義的執行的一致性(單執行緒),但並不會關心多執行緒間的語義一致性。所以當一條執行緒訪問singleton不為null時,由於singleton例項未必已初始化完成,也就造成了執行緒安全問題,volatile禁止singleton變數被執行指令重排優化.

volatile重排序規則表

可以總結為三條:

  1. 當第二個操作是volatile 寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile 寫之前的操作不會被編譯器重排序到volatile 寫之後。
  2. 當第一個操作是volatile 讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile 讀之後的操作不會被編譯器重排序到volatile 讀之前。
  3. 當第一個操作是volatile 寫,第二個操作是volatile 讀時,不能重排序。

最後

感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!