Java併發—— 關鍵字volatile解析

簡述

關鍵字volatile能夠說是Java虛擬機提供的最輕量級的同步機制,當一個變量定義爲volatile,它具備內存可見性以及禁止指令重排序兩大特性,爲了更好地瞭解volatile關鍵字,咱們能夠先看Java內存模型java

Java內存模型

Java內存模型規定了全部的變量都存儲在主內存中,每條線程擁有本身的工做內存,工做內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀寫)都必須在工做內存中進行,不一樣的線程之間沒法直接訪問對方工做內存的變量。線程、主內存、工做內存關係:面試

以經典的i++爲例,線程A從主內存獲取變量i值放入到工做內存的變量副本,而後在工做內存中將i+1,最後將新值同步到主內存中。從中咱們能夠看出簡單的i++,分了3個步驟,能夠明顯發如今線程A從主內存獲取i值步驟後,可能有其餘線程同步主內存中變量i的值,當線程A想要將i+1結果同步到主內存時就會出現不正確的結果,這是典型的線程不安全。編程

volatile特性

  • 可見性
  • 當一個線程修改了共享變量,其餘線程可以當即得知這個修改。Java內存模型經過在變量修改後將新值同步回主內存,volatile變量能保證新值能當即同步到主內存,以及每次使用前當即從主內存刷新(synchronized和final兩個關鍵字也具有)。仍是拿i++爲例,volatile修飾的i能夠確保,從主存中所獲取的變量i必定是最新的。安全

  • 有序性
  • 禁止指令重排序,程序執行的順序按照代碼的前後順序執行。
    在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序。
    ①.編譯器重排序:編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序
    ②.處理器重排序:若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序

    從java源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
    架構

    1屬於編譯器重排序,2和3屬於處理器重排序。併發

    volatile使用場景

    在某些特定場景中,volatile至關於一個輕量級的sychronize,由於不會引發線程的上下文切換,可是使用volatile必須知足兩個條件:
    ①.運算結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值
    ②.變量不須要與其餘的狀態變量共同參與不變約束

    兩個使用場景:
    異步

  • 狀態標記
  • 使用volatile變量來控制併發,當shutdown()方法被調用時,能保證全部線程中執行的doWork()方法都當即停下來

    性能

    public class VolatileTest { private volatile boolean shutdownRequested;
       public void shutdown() {
           shutdownRequested = true;
       }
    
       public void doWork(){
           while (!shutdownRequested) {
            // 業務邏輯
          }    
      }
    }
    複製代碼
    複製代碼public void shutdown() { shutdownRequested = true; } public void doWork(){ while (!shutdownRequested) { // 業務邏輯 } } } 複製代碼複製代碼

  • DCL(雙鎖檢測)
  • 單例模式的一種實現方式

    spa

    public class Singleton {
    
        private volatile static Singleton singleton;
    
        public static Singleton getInstance() {
            if(singleton == null){
                synchronized (Singleton.class){
                    if(singleton == null){
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    複製代碼
    複製代碼public class Singleton { private volatile static Singleton singleton; public static Singleton getInstance() { if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } } 複製代碼複製代碼

    volatile實現

    從硬件架構上來說,處理器使用寫緩衝區來臨時保存向內存寫入的數據,能夠減小對內存總線的佔用。雖然寫緩衝區有這麼多好處,但此操做僅對它所在的處理器可見,這個特性會對內存操做的執行順序產生重要的影響,因爲操做緩衝區是異步操做因此在外面看來,先寫後讀,仍是先讀後寫,沒有嚴格的固定順序。
    線程

    volatile修飾的變量相對於普通變量會多出一個lock前綴指令,這個操做至關於一個內存屏障(只有一個CPU訪問內存時,不須要內存屏障;但若是有兩個或更多CPU訪問同一塊內存,且其中有一個在觀測另外一個,就須要內存屏障來保證一致性)。

    是否能重排序 第二個操做
    第一個操做 普通讀 普通寫 volatile讀 volatile寫
    普通讀 LoadStore
    普通寫 StoreStore
    volatile讀 LoadLoad LoadStore LoadLoad LoadStore
    volatile寫 StoreLoad StoreStore
    空白的單元格表明在不違反Java的基本語義下的重排是容許的。
    StoreStore屏障:保證在volatile寫以前,其前面的全部普通寫操做都已經刷新到主內存
    StoreLoad屏障:避免volatile寫與後面可能有的volatile讀/寫操做重排序
    LoadLoad屏障:禁止處理器吧上面的volatile讀與下面的普通讀中排序
    LoadStore屏障:禁止處理器把上面的volatile讀與下面的普通寫重排序

    示例:

    public class VolatileTest { int a = 0; volatile int var1 = 1; volatile int var2 = 2;
    void readAndWrite() {
        int i = var1;   //volatile讀
        int j = var2;   //volatile讀
        a = i + i;      //普通讀
        var1 = i + 1;   //volatile寫
        var2 = j * 2;   //volatile寫
    }
    複製代碼
    複製代碼void readAndWrite() { int i = var1; //volatile讀 int j = var2; //volatile讀 a = i + i; //普通讀 var1 = i + 1; //volatile寫 var2 = j * 2; //volatile寫 } 複製代碼} 複製代碼
    大體過程:

    感謝

    1.《深刻理解Java虛擬機》
    2.佔小狼——面試必問的volatile,你瞭解多少? 3.《Java併發編程的藝術》

    相關文章
    相關標籤/搜索