Java volatile關鍵字作用

隨筆發燒友發表於2018-11-02

當一個共享變數被volatile修飾時,它會保證修改的值立即被更新到主存“, 這裡的”保證“ 是如何做到的?和 JIT的具體編譯後的CPU指令相關吧?

  volatile特性

  記憶體可見性:通俗來說就是,執行緒A對一個volatile變數的修改,對於其它執行緒來說是可見的,即執行緒每次獲取volatile變數的值都是最新的。

  volatile的使用場景

  通過關鍵字sychronize可以防止多個執行緒進入同一段程式碼,在某些特定場景中,volatile相當於一個輕量級的sychronize,因為不會引起執行緒的上下文切換,但是使用volatile必須滿足兩個條件:

  1、對變數的寫操作不依賴當前值,如多執行緒下執行a++,是無法通過volatile保證結果準確性的;

  2、該變數沒有包含在具有其它變數的不變式中,這句話有點拗口,看程式碼比較直觀。

public class NumberRange {
  private volatile int lower = 0;

  private volatile int upper = 10;

  public int getLower() { return lower; }

  public int getUpper() { return upper; }

  public void setLower(int value) {

     if (value> upper)

     throw new IllegalArgumentException(...);

     lower = value;

  }

  public void setUpper(int value) {

     if (value < lower)

     throw new IllegalArgumentException(...);

     upper = value;

   }
}

上述程式碼中,上下界初始化分別為0和10,假設執行緒A和B在某一時刻同時執行了setLower(8)和setUpper(5),且都通過了不變式的檢查,設定了一個無效範圍(8, 5),所以在這種場景下,需要通過sychronize保證方法setLower和setUpper在每一時刻只有一個執行緒能夠執行。

上述如果沒有了解volatile的作用,那麼看下下面的例子可以看出volatile在實際中的作用

下面是我們在專案中經常會用到volatile關鍵字的兩個場景:

  1、狀態標記量

  在高併發的場景中,通過一個boolean型別的變數isopen,控制程式碼是否走促銷邏輯,該如何實現?

public class ServerHandler {

      private volatile isopen;

      public void run() {
         
        if (isopen) {

            //促銷邏輯
              
        } else {

           //正常邏輯
              
        }
          
    }

    public void setIsopen(boolean isopen) {

       this.isopen = isopen
          
    }      
}

上述一個簡單的案例我們可以清楚的看到,現實場景中使用者執行了多執行緒中run()方法,如果需要開啟促銷邏輯,那麼只需要後臺設定呼叫setIsopen(true) 方法,就能很好的控制多執行緒中方法控制的問題了,該放說明volatile關鍵字的作用就是告訴該執行方法時時獲取最新變數值。

 如何保證記憶體可見性?

  在java虛擬機器的記憶體模型中,有主記憶體和工作記憶體的概念每個執行緒對應一個工作記憶體,並共享主記憶體的資料,下面看看操作普通變數和volatile變數有什麼不同:

  1、對於普通變數:讀操作會優先讀取工作記憶體的資料,如果工作記憶體中不存在,則從主記憶體中拷貝一份資料到工作記憶體中;寫操作只會修改工作記憶體的副本資料,這種情況下,其它執行緒就無法讀取變數的最新值。

  2、對於volatile變數,讀操作時JMM會把工作記憶體中對應的值設為無效,要求執行緒從主記憶體中讀取資料;寫操作時JMM會把工作記憶體中對應的資料重新整理到主記憶體中,這種情況下,其它執行緒就可以讀取變數的最新值。

 

volatile變數的記憶體可見性是基於記憶體屏障(Memory Barrier)實現的,什麼是記憶體屏障?記憶體屏障,又稱記憶體柵欄,是一個CPU指令。在程式執行時,為了提高執行效能,編譯器和處理器會對指令進行重排序,JMM為了保證在不同的編譯器和CPU上有相同的結果,通過插入特定型別的記憶體屏障來禁止特定型別的編譯器重排序和處理器重排序,插入一條記憶體屏障會告訴編譯器和CPU:不管什麼指令都不能和這條Memory Barrier指令重排序。

 

class Singleton {

      private volatile static Singleton instance;

      private int a;

      private int b;

      private int b;

      public static Singleton getInstance() {
          
        if (instance == null) {

              syschronized(Singleton.class) {
                  
                if (instance == null) {

                      a = 1; // 1
                      b = 2; // 2
                      instance = new Singleton(); // 3
                      c = a + b; // 4
                }
                  
            }
              
        }
 
        return instance;  
    } 
}

1、如果變數instance沒有volatile修飾,語句1、2、3可以隨意的進行重排序執行,即指令執行過程可能是3214或1324。

2、如果是volatile修飾的變數instance,會在語句3的前後各插入一個記憶體屏障。

相關文章