天天看點

Volatile 關鍵字

一、 定義

  • Java虛拟機提供的最輕量級的同步機制。

二、通過volatile關鍵字修飾後,具備兩種特性

  • 保證此變量對所有線程的可見性,這裡的“可見性”是指當一條線程修改了這個變量的值,新值對于其他線程來說是可以 立即得知的。

    普通變量不能做到這一點,普通變量的值線上程間傳遞均需要通過主記憶體來完成,例如,線程A修改一個普通變量的值,

    然後向主記憶體進行回寫,另外一條線程B線上程A回寫完成了之後再從主記憶體進行讀取操作,新變量值才會對線程B可見。

  • 禁止指令重排序優化。
    • 通過一個單例模式來講解,代碼如下:
    public class Monitors {
              private static volatile Monitors monitors = null; 
              private Monitors() {}
               public static Monitors getMonitor() {
                      if (monitor == null) {
                              synchronized (Monitors.class) {
                                  if (monitors == null) {
                                      monitors = new Monitors();
                                  } 
                              }
                      }
                      return monitors;
               }
       }           
    • 分析代碼
      • monitors = new Monitors();,在這個操作中,JVM主要做三件事:

        1、在java 堆空間裡配置設定一部分空間;

    2、執行 Monitor 的構造方法進行初始化;

    3、把 monitor 對象指向在堆空間中配置設定好的空間。

    三步執行完後,這個 monitor 對象就不是空值。

    • 普通變量不能保證變量指派操作的順序與程式代碼中的執行順序一緻,指令重排序的優化很可能改變程式的執行順序。比如,執行順序可能為

      1、2、3,也可能為1、3、2。如果是按照1、3、2的順序執行,恰巧在執行到3的時候(還沒執行2),新的線程執行 getMonitor() 方法之後判斷

    monitor 不為空就傳回了 monitor 執行個體。此時 monitor 執行個體雖有值, 但它沒有執行構造方法進行初始化(即沒有執行2),

    故該線程如果對那些需要初始化的參數進行操作就會發生錯誤。

    但是加volatile 關鍵字的話,就不會出現這個問題。禁止指令重排序優化。 
                 
    • 分析class位元組碼檔案得知
      • 有volatile修飾的變量,指派後多執行了一個“lock addl $0x0,(%esp)”操作,這個操作相當于一個記憶體屏障

        (Memory Barrier或Memory Fence,指重排序時不能把後 面的指令重排序到記憶體屏障之前的位置),

    隻有一個CPU通路記憶體時,并不需要記憶體屏障;但如果有兩個或更多CPU通路同一塊記憶體,且其中有一個在觀測另一個,就需要記憶體屏障來保證一緻性了。
    • lock字首,查詢IA32手冊,它的作用是使得本CPU的Cache寫入了記憶體,該寫入動作也會引起别的CPU或者别的核心無效化(Invalidate)其Cache,

      這種操作相當于對Cache中的變量做了一次前面介紹Java記憶體模式中所說的“store和write”操作。

    是以通過這樣一個空操作,可讓前面volatile變量的修改對其他CPU立即可見。

    store(存儲):作用于工作記憶體的變量,它把工作記憶體中一個變量的值傳送到主記憶體 中,以便随後的write操作使用。

    write(寫入):作用于主記憶體的變量,它把store操作從工作記憶體中得到的變量的值放入 主記憶體的變量中。

    • 那為何說它禁止指令重排序呢?
      • 從硬體架構上講,指令重排序是指CPU采用了允許将多條指令不按程式規定的順序分開發送給各相應電路單元處理。但并不是說指令任意重排,

        CPU需要能正确處理指令依賴情況以保障程式能得出正确的執行結果。

      • 譬如指令1把地 址A中的值加10,指令2把位址A中的值乘以2,指令3把位址B中的值減去3,這時指令1和指 令2是有依賴的,它們之間的順序

        不能重排——(A+10)2與A2+10顯然不相等,但指令3 可以重排到指令1、2之前或者中間,隻要保證CPU執行後面依賴到A、B值的操作時能擷取到 正确的A和B值即可。

    是以在本内CPU中,重排序看起來依然是有序的。
    • 是以,lock addl$0x0,(%esp)指令把修改同步到記憶體時,意味着所有之前的操作都已經執行完成, 這樣便形成了“指令重排序無法越過記憶體屏障”的效果。

三、 使用條件

  1. 滿足如下兩個條件:
    • 運算結果并不依賴變量的目前值,或者能夠確定隻有單一的線程修改變量的值。
    • 變量不需要與其他的狀态變量共同參與不變限制。
  2. 通過加鎖(使用synchronized或java.util.concurrent中的原子類)
  • 我的了解就是: 保證操作是原子性操作。

原因,在java虛拟機中解釋:

  • volatile變量在各個線程的工作記憶體中不存在一緻性問題(在各個線程 的工作記憶體中,volatile變量也可以存在不一緻的情況,
    但由于每次使用之前都要先重新整理,執行引擎看不到不一緻的情況,是以可以認為不存在一緻性問題),但是Java裡面的運算并非原子操作,
      導緻volatile變量的運算在并發下一樣是不安全的。
                 
  1. 通過一段簡單的示範,代碼清單如下:
    public class Volatile_test {
     public static volatile int race = 0;
     public static void increase(){
         race ++;
     }
     private static final int THREADS_COUNT = 20;
     public static void main(String [] args ){
    
         Thread [] threads = new Thread[THREADS_COUNT];
         for(int i=0;i<THREADS_COUNT;i++)
         {
             threads[i] = new Thread( new Runnable(){
    
                 @Override public void run(){
                 for(int i =0 ; i<10000 ; i++)
                 {
                     increase();
                 }}
             });
             threads[i].start();
         }
         //等待所有累加線程都結束
         while(Thread.activeCount() > 1)
          Thread.yield();
         System.out.println(race);
     }
    }           
  2. 結果分析,這段代碼開啟20個線程,每個線程對race的自增做10000次操作,理論上輸出的race為200000,但是實際運作結果總是小于200000。
  3. 推測問題出現在race ++ 這行代碼中,通過反編譯這個類得到代碼清單,發現隻有一行代碼的increase()方法在Class檔案中是由4條位元組碼指令構成的。
    反編譯位元組碼如下:           
    * public static void increase();
       * Code:
       * Stack=2,Locals=0,Args_size=0
       * 0:getstatic # 13; //Field race:I
       * 3:iconst_1
       * 4:iadd
       * 5:putstatic # 13;//Field race:I
       * 8:return LineNumberTable:
       * line 14:0
       * line 15:8           
    通過位元組碼層次分析:    
                 
    • 當getstatic指令把race的值取到操作棧頂時,volatile關鍵字保證了race的值在此 時是正确的,但是在執行iconst_1、iadd這些指令的時候,
      其他線程可能已經把race的值加大,而在操作棧頂的值就變成了過期的資料,是以putstatic指令執行後就可能把較小的race值同步回主記憶體之中。           

四、 volatile和鎖的比較

  • 在某些情況下,volatile的同步機制的性能确實要優于鎖(使用synchronized關鍵字或java.util.concurrent包裡面的鎖),

    但是由于虛拟機對鎖實行的許多消除和優化,使得我們很難量化地認為volatile就會比synchronized快多少;

  • volatile變量讀操作的性能消耗與普通變量幾乎沒有什麼差别,但是寫操作則可能會慢一些,因為它需要在本地代碼中插入許多記憶體屏障指令來保證處理器不發生亂序執行。
  • 大多數場景下volatile的總開銷要比鎖低;
  • 在volatile與鎖之中選擇的唯一依據僅僅是volatile的語義能否滿足使用場景的需求。