多線程 同步 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的語義能否滿足使用場景的需求。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章