Volatile筆記

什麼是volatile?

1、volatile 類型修飾符。使用如下:

 volatile int i;

2、volatile是多線程的一種解決方案,使用volatile定義的變量,多線程可見。

volatile 特點:

1:可見性。
2:禁止重排序。

實現原理

1:可見性

線程本身並不直接與主內存進行數據的交互,而是通過線程的工作內存來完成相應的操作。如下圖所示
在這裏插入圖片描述
這也是導致線程間數據不可見的本質原因。因此要實現volatile變量的可見性,直接從這方面入手即可。對volatile變量的寫操作與普通變量的主要區別有兩點:
  (1)修改volatile變量時會強制將修改後的值刷新的主內存中。
  (2)修改volatile變量後會導致其他線程工作內存中對應的變量值失效。再讀取該變量值的時候就需要重新從讀取主內存中的值。
  如下圖所示。
在這裏插入圖片描述

2:禁止重排序

什麼是重排序?爲了性能優化,JVM在不改變正確語義的前提下,會允許編譯器和處理器對指令序列進行重排序。參考-指令重排序
那怎麼才能防止指令重排序呢?內存屏障
JMM內存屏障分爲四類見下圖,在這裏插入圖片描述
java編譯器會在生成指令系列時在適當的位置會插入內存屏障指令來禁止特定類型的處理器重排序。爲了實現volatile的內存語義,JMM會限制特定類型的編譯器和處理器重排序,JMM會針對編譯器制定volatile重排序規則表:
在這裏插入圖片描述
"NO"表示禁止重排序。爲了實現volatile內存語義時,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。對於編譯器來說,發現一個最優佈置來最小化插入屏障的總數幾乎是不可能的,爲此,JMM 採取了保守策略:

  1. 在每個volatile寫操作的前面插入一個StoreStore屏障;
  2. 在每個volatile寫操作的後面插入一個StoreLoad屏障;
  3. 在每個volatile讀操作的後面插入一個LoadLoad屏障;
  4. 在每個volatile讀操作的後面插入一個LoadStore屏障。
    需要注意的是:volatile寫是在前面和後面分別插入內存屏障,而volatile讀操作是在後面插入兩個內存屏障
    StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序;
    StoreLoad屏障:防止上面的volatile寫與下面可能有的volatile讀/寫重排序
    LoadLoad屏障:禁止下面所有的普通讀操作和上面的volatile讀重排序
    LoadStore屏障:禁止下面所有的普通寫操作和上面的volatile讀重排序
    下面以兩個示意圖進行理解,圖片摘自相當好的一本書《java併發編程的藝術》。

在這裏插入圖片描述

在這裏插入圖片描述

多線程可見性測試

代碼如下:

public class VolatileDemo {

    //private static boolean isOver = false;  不用volatile
    private static volatile boolean isOver = false;  //使用volatile
    
    public static void main(String[] args) {
        //線程A
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isOver) 
            }
        },"A");
        thread.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isOver = true;
    }
}

不用 volatile時,isOver一直是false,即便後面改成true,循環依賴沒有停止。

使用 volatile時,線程A能識別到 isOver的改變,並重新加載值,循環停止。

多線程原子性測試

public class VolatileTest01 {
    volatile int i;

    public void addI(){
        //i++;
        i=i+1;
    }

    public static void main(String[] args) throws InterruptedException {
        final  VolatileTest01 test01 = new VolatileTest01();
        for (int n = 0; n < 1000; n++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test01.addI();
                }
            }).start();
        }
        Thread.sleep(2000);//等待2秒,保證上面程序執行完成
        System.out.println(test01.i);
    }
}

得出結果:

929

PS:重複試,沒有一次結果是對的。

測試結論:

volatile 能保證多線程的可見性,防止數據髒讀。卻不能保證原子性。

volatile 應用場景

根據volatile,可見性,非原子性的特點,可以用於以下場景

1:狀態標誌

也許實現 volatile 變量的規範使用僅僅是使用一個布爾狀態標誌,用於指示發生了一個重要的一次性事件,例如完成初始化或請求停機。

volatile boolean shutdownRequested;
......
public void shutdown() { shutdownRequested = true; }
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

2: 一次性安全發佈(one-time safe publication)

缺乏同步會導致無法實現可見性,這使得確定何時寫入對象引用而不是原始值變得更加困難。在缺乏同步的情況下,可能會遇到某個對象引用的更新值(由另一個線程寫入)和該對象狀態的舊值同時存在。(這就是造成著名的雙重檢查鎖定(double-checked-locking)問題的根源,其中對象引用在沒有同步的情況下進行讀操作,產生的問題是您可能會看到一個更新的引用,但是仍然會通過該引用看到不完全構造的對象)。

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;
 
    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}
 
public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}

3:獨立觀察(independent observation)

安全使用 volatile 的另一種簡單模式是定期 發佈 觀察結果供程序內部使用。例如,假設有一種環境傳感器能夠感覺環境溫度。一個後臺線程可能會每隔幾秒讀取一次該傳感器,並更新包含當前文檔的 volatile 變量。然後,其他線程可以讀取這個變量,從而隨時能夠看到最新的溫度值。

public class UserManager {
    public volatile String lastUser;
 
    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}

4:雙重檢查(double-checked)

單例模式的一種實現方式,但很多人會忽略 volatile 關鍵字,因爲沒有該關鍵字,程序也可以很好的運行,只不過代碼的穩定性總不是 100%,說不定在未來的某個時刻,隱藏的 bug 就出來了。

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

本人能力有限,如有錯誤請指出。
參考
https://www.jianshu.com/p/157279e6efdb
https://blog.csdn.net/devotion987/article/details/68486942

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