一:Volatile的理解

一、輕量級的同步機制

  • 保證可見性
  • 禁止指令排序
  • 不保證原子性

二、基本概念

  • JMM 本身是一種抽象的概念並不是真實存在,它描述的是一組規定或則規範,通過這組規範定義了程序中的訪問方式。
  • JMM 同步規定
    • 線程解鎖前,必須把共享變量的值刷新回主內存
    • 線程加鎖前,必須讀取主內存的最新值到自己的工作內存
    • 加鎖解鎖是同一把鎖
  • 由於 JVM 運行程序的實體是線程,而每個線程創建時 JVM 都會爲其創建一個工作內存,工作內存是每個線程的私有數據區域,而 Java 內存模型中規定所有變量的儲存在主內存,主內存是共享內存區域,所有的線程都可以訪問,但線程對變量的操作(讀取賦值等)必須都工作內存進行看。
  • 首先要將變量從主內存拷貝的自己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回主內存,不能直接操作主內存中的變量,工作內存中存儲着主內存中的變量副本拷貝,前面說過,工作內存是每個線程的私有數據區域,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成。
  • 內存模型圖
    搜狗截圖20190416211412

三、特性

  • 可見性

    public class VolatileDemo {
        public static void main(String[] args) {
            Data data = new Data();
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " coming...");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                data.addOne();
                System.out.println(Thread.currentThread().getName() + " updated...");
            }).start();
    
            while (data.a == 0) {
                // looping
            }
            System.out.println(Thread.currentThread().getName() + " job is done...");
        }
    }
    
    class Data {
        // int a = 0;
        volatile int a = 0;
        void addOne() {
            this.a += 1;
        }
    }
    

    如果不加 volatile 關鍵字,則主線程會進入死循環,加 volatile 則主線程能夠退出,說明加了 volatile 關鍵字變量,當有一個線程修改了值,會馬上被另一個線程感知到,當前值作廢,從新從主內存中獲取值。對其他線程可見,這就叫可見性。

  • 不可原子性

    public class VolatileDemo {
        public static void main(String[] args) {
           // test01();
           test02();
        }
    
        // 測試原子性
        private static void test02() {
            Data data = new Data();
            for (int i = 0; i < 20; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 1000; j++) {
                        data.addOne();
                    }
                }).start();
            }
            // 默認有 main 線程和 gc 線程
            while (Thread.activeCount() > 2) {
                Thread.yield();
            }
            System.out.println(data.a);
        }
    }
    
    class Data {
        volatile int a = 0;
        void addOne() {
            this.a += 1;
        }
    }
    

    發現並不能輸出 20000

  • 有序性

    • 計算機在執行程序時,爲了提高性能,編譯器個處理器常常會對指令做重排,一般分爲以下 3 種

      • 編譯器優化的重排
      • 指令並行的重排
      • 內存系統的重排
    • 單線程環境裏面確保程序最終執行的結果和代碼執行的結果一致

    • 處理器在進行重排序時必須考慮指令之間的數據依賴性

    • 多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證用的變量能否一致性是無法確定的,結果無法預測

    • 代碼示例

      public class ReSortSeqDemo {
          int a = 0;
          boolean flag = false;
          
          public void method01() {
              a = 1;           // flag = true;
                               // ----線程切換----
              flag = true;     // a = 1;
          }
      
          public void method02() {
              if (flag) {
                  a = a + 3;
                  System.out.println("a = " + a);
              }
          }
      
      }
      

      如果兩個線程同時執行,method01 和 method02 如果線程 1 執行 method01 重排序了,然後切換的線程 2 執行 method02 就會出現不一樣的結果。

四、禁止指令排序

volatile 實現禁止指令重排序的優化,從而避免了多線程環境下程序出現亂序的現象

先了解一個概念,內存屏障(Memory Barrier)又稱內存柵欄,是一個 CPU 指令,他的作用有兩個:

  • 保證特定操作的執行順序
  • 保證某些變量的內存可見性(利用該特性實現 volatile 的內存可見性)

由於編譯器個處理器都能執行指令重排序優化,如果在指令間插入一條 Memory Barrier 則會告訴編譯器和 CPU,不管什麼指令都不能將這條 Memory Barrier 指令重排序,也就是說通過插入內存屏障禁止在內存屏障前後執行重排序優化。內存屏障另一個作用是強制刷出各種 CPU 緩存數據,因此任何 CPU 上的線程都能讀取到這些數據的最新版本。

下面是保守策略下,volatile寫插入內存屏障後生成的指令序列示意圖:

0e75180bf35c40e2921493d0bf6bd684_th

下面是在保守策略下,volatile讀插入內存屏障後生成的指令序列示意圖:

21ebc7e8190c4966948c4ef4424088be_th

五、線程安全性保證

  • 工作內存與主內存同步延遲現象導致可見性問題
    • 可以使用 synchronzied 或 volatile 關鍵字解決,它們可以使用一個線程修改後的變量立即對其他線程可見
  • 對於指令重排導致可見性問題和有序性問題
    • 可以利用 volatile 關鍵字解決,因爲 volatile 的另一個作用就是禁止指令重排序優化

六、volatile案例

單例

  • 多線程環境下可能存在的安全問題

    @NotThreadSafe
    public class Singleton01 {
        private static Singleton01 instance = null;
        private Singleton01() {
            System.out.println(Thread.currentThread().getName() + "  construction...");
        }
        public static Singleton01 getInstance() {
            if (instance == null) {
                instance = new Singleton01();
            }
            return instance;
        }
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) {
                executorService.execute(()-> Singleton01.getInstance());
            }
            executorService.shutdown();
        }
    }
    

    發現構造器裏的內容會多次輸出

  • 雙重鎖單例

    • 代碼
    public class Singleton02 {
        private static volatile Singleton02 instance = null;
        private Singleton02() {
            System.out.println(Thread.currentThread().getName() + "  construction...");
        }
        public static Singleton02 getInstance() {
            if (instance == null) {
                synchronized (Singleton01.class) {
                    if (instance == null) {
                        instance = new Singleton02();
                    }
                }
            }
            return instance;
        }
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) {
                executorService.execute(()-> Singleton02.getInstance());
            }
            executorService.shutdown();
        }
    }
    
    • 如果沒有加 volatile 就不一定是線程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。

    • 原因是在於某一個線程執行到第一次檢測,讀取到的 instance 不爲 null 時,instance 的引用對象可能還沒有完成初始化。

    • instance = new Singleton() 可以分爲以下三步完成

    memory = allocate();  // 1.分配對象空間
    instance(memory);     // 2.初始化對象
 	instance = memory;   // 3.設置instance指向剛分配的內存地址,此時instance != null
  • 步驟 2 和步驟 3 不存在依賴關係,而且無論重排前還是重排後程序的執行結果在單線程中並沒有改變,因此這種優化是允許的。

  • 發生重排

    memory = allocate();  // 1.分配對象空間
    instance = memory;    // 3.設置instance指向剛分配的內存地址,此時instance != null,但對象還沒有初始化完成
    instance(memory);     // 2.初始化對象
    
  • 所以不加 volatile 返回的實例不爲空,但可能是未初始化的實例
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章