Voliate關鍵字

zhangyf1121發表於2024-06-30

Voliate關鍵字

禁止執行緒快取變數結果。 可見性問題主要指一個執行緒修改了共享變數值,而另一個執行緒卻看不到。 引起可見性問題的主要原因是每個執行緒擁有自己的一個快取記憶體區——執行緒工作記憶體

1.Voliate保證可見性

  • 不使用volatile關鍵字
public class Test {

    private static Boolean stop = false;

    public static void main(String[] args) throws InterruptedException {

        // Thread-A
        new Thread("Thread A") {
            @Override
            public void run() {
                while (!stop) {
                }
                System.out.println(Thread.currentThread() + " stopped");
            }
        }.start();

        Thread.sleep(100);

        // Thread-B
        new Thread("Thread B") {
            @Override
            public void run() {
                stop = true;
                System.out.println("started");
            }
        }.start();
    }
}

一個執行緒內使用了停止的開關,假如這個stop沒有被volatile修飾,我們線上程b中修改,執行緒a並不知道開關的值被修改了
    
---------------------------------------------------------------|-
 console   							     |
---------------------------------------------------------------|-
started    							       |
                                                                            |
---------------------------------------------------------------|-                                                                            

2.Voliate保證有序性

  • 使用volatile 防重排序

從一個最經典的例子來分析重排序問題。大家應該都很熟悉單例模式的實現
public class Singleton {
public static volatile Singleton singleton;
/**
* 建構函式私有,禁止外部例項化
*/
private Singleton() {};
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

例項化一個物件其實可以分為三個步驟:

  1. 分配記憶體空間

  2. 初始化物件

  3. 將記憶體空間的地址賦值給對應的引用

但是由於作業系統可以對指令進行重排序,所以上面的過程也可能會變成如下過程:

  1. 分配記憶體空間

  2. 將記憶體空間的地址賦值給對應的引用

  3. 初始化物件

如果是這個流程,多執行緒環境下就可能將一個未初始化的物件引用暴露出來,從而導致不可預料的結果。
因此,為了防止這個過程的重排序,我們需要將變數設定為volatile型別的變數

  • 使用volatile 保證原子性:單次讀/寫
  1. 對volatile變數的單次讀/寫操作可以保證原子性的,如long和double型別變數
  2. 但是並不能保證i++這種操作的原子性,因為本質上i++是讀、寫兩次操作,要保證多步的原子性
  3. 可以透過AtomicInteger或者Synchronized來實現,本質上就是cas操作

3. 問題

3.1.i++為什麼不能保證原子性?

i++其實是一個複合操作,包括三步驟:

  1. 讀取i的值

  2. 對i加1

  3. 將i的值寫回記憶體

  4. volatile是無法保證這三個操作是具有原子性的

  5. 我們可以透過AtomicInteger或者Synchronized來保證+1操作的原子性

3.2.共享的long和double變數的為什麼要用volatile?

  1. 因為long和double兩種資料型別的操作可分為高32位和低32位兩部分,
  2. 因此普通的long或double型別讀/寫可能不是原子的。
  3. 因此,鼓勵大家將共享的long和double變數設定為volatile型別,
  4. 這樣能保證任何情況下對long和double的單次讀/寫操作都具有原子性

4.總結

  1. 在某些操作下,voliate確實是可以保證原子性的。但設計之處,voliate並不是用於保證原子性
  2. 在複合操作下,例如i++,A執行緒執行完在i+1的操作但是還沒寫到記憶體的時候,即使使用了voliate保證了i的可見性,但是B執行緒此時看到的資料還是舊資料,那麼就有可能出現資料錯誤