Java中的 volatile 關鍵字

說這個之前,要先說到cpu的運行,大家都知道,計算機在執行程序時,每條指令都是在 CPU 中執行的,而執行指令過程中,勢必涉及到數據的讀取和寫入。由於程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由於CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟 CPU 執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。因此在 CPU 裏面就有了高速緩存。如果程序中存在有被多個線程訪問的變量也就是共享變量,有可能就會造成緩存一致性問題(有個緩存一致性協議,不過,他是硬件層面的,叫MESI 協議)。
在多核 CPU 中(現在應該都是多核了),每條線程可能運行於不同的 CPU 中,因此每個線程運行時有自己的高速緩存,所以高速緩存中的變量沒有立即寫入主存,就會造成可見性問題(也就是其他線程不可見)。
爲了解決可見性問題:
第一張方法:就是這裏的volatile 關鍵字,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
第二種:通過 synchronized 和 Lock 也能夠保證可見性,synchronized 和 Lock 能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
爲了解決緩存不一致性問題:
使用緩存一致性協議(硬件層面)。最出名的就是 Intel 的 MESI 協議,MESI 協議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當 CPU 寫數據時,如果發現操作的變量是共享變量,即在其他 CPU 中也存在該變量的副本,會發出信號通知其他 CPU 將該變量的緩存行置爲無效狀態(反映到硬件層的話,就是CPU 的 L1 或者 L2 緩存中對應的緩存行無效),因此當其他 CPU 需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。

volatile還跟變量的原子性有關,有個經典的自增問題(i++),即使用鎖或者volatile都不能保證原子性。要想解決就得用原子類,在 java 1.5的 java.util.concurrent.atomic 包下提供了一些原子操作類,即對基本數據類型的 自增(加 1操作),自減(減 1 操作)、以及加法操作(加一個數),減法操作(減一個數)進行了封裝,保證這些操作是原子性操作。atomic 是利用 CAS 來實現原子性操作的(Compare And Swap),CAS 實際上是利用處理器提供的 CMPXCHG 指令實現的,而處理器執行 CMPXCHG 指令是一個原子性操作。
還有一點, volatile 關鍵字能禁止指令重排序,所以 volatile 能在一定程度上保證有序性。
舉個例子:

//x、y爲非volatile變量
//flag爲volatile變量
 
x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;         //語句4
y = -1;       //語句5

由於 flag 變量爲 volatile 變量,那麼在進行指令重排序的過程的時候,不會將語句 3 放到語句 1、語句 2 前面,也不會講語句 3 放到語句 4、語句 5 後面。但是要注意語句 1 和語句 2 的順序、語句 4 和語句 5 的順序是不作任何保證的。
並且 volatile 關鍵字能保證,執行到語句 3 時,語句 1 和語句 2 必定是執行完畢了的,且語句 1 和語句 2 的執行結果對語句 3、語句 4、語句 5 是可見的。
說一下原理:volatile 的原理和實現機制:
觀察加入 volatile 關鍵字和沒有加入 volatile 關鍵字時所生成的彙編代碼發現,加入 volatile 關鍵字時,會多出一個 lock 前綴指令
lock 前綴指令實際上相當於一個內存屏障(也稱內存柵欄),內存屏障會提供 3 個功能:
1.它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
2.它會強制將對緩存的修改操作立即寫入主存;
3.如果是寫操作,它會導致其他 CPU 中對應的緩存行無效。
volatile的金典使用場景:雙重檢查

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;
    }
}

據說這樣的雙重檢查有問題,可以使用ThreadLocal修復雙重檢測

public class Singleton {  
 private static final ThreadLocal perThreadInstance = new ThreadLocal();  
 private static Singleton singleton ;  
 private Singleton() {}  
   
 public static Singleton  getInstance() {  
  if (perThreadInstance.get() == null){  
   // 每個線程第一次都會調用  
   createInstance();  
  }  
  return singleton;  
 }  
  
 private static  final void createInstance() {  
  synchronized (Singleton.class) {  
   if (singleton == null){  
    singleton = new Singleton();  
   }  
  }  
  perThreadInstance.set(perThreadInstance);  
 }  
}

有個原子性操作案例:

x = 10;         //語句1
y = x;         //語句2
x++;           //語句3
x = x + 1;     //語句4

有些朋友可能會說上面的 4 個語句中的操作都是原子性操作。其實只有語句 1 是原子性操作,其他三個語句都不是原子性操作。

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