volatile原理和應用場景

volatile是java語言中的一個關鍵字,經常使用於併發編程,有兩個重要的特色:具備可見性,java虛擬機實現會爲其知足Happens before原則;不具有原子性.用法是修飾變量,如:volatile int i.html

volatile原理

介紹其可見性先從cpu,cpu緩存和內存的關係入手.java

cpu緩存是一種加速手段,cpu查找數據時會先從緩存中查找,若是不存在會從內存中查找,因此若是緩存中數據和內存中數據不一致,cpu處理數據的一致性就沒法保證.從機器語言角度來說,有一些一致性協議來保證緩存一致,可是本文主要從抽象角度解釋volatile爲什麼能保證可見性.對於volatile變量的賦值,會刷入主內存,而且通知其餘cpu核心,大家緩存中的數據無效了,這樣全部cpu核心再想對該volatile變量操做首先會從主內存中從新拉取值.這就保證了對於cpu操做的數據是最新.編程

可是這並不能保證volatile修飾的變量的原子性.讓咱們想一想一個場景,變量volatile int count存儲在內存中,cpu核心1和cpu核心2同時讀取該數據,並存入緩存,而後進行count++操做.count++實際能夠分解爲三步:緩存

int tmp = count;
tmp = count + 1;
count = tmp;

count = tmp執行結束,cpu會把count刷入內存並通知其餘cpu緩存無效,若是兩個cpu核心同時將其刷入了內存,通知了緩存無效,那麼咱們是否是隻獲得了count = 2,是否是丟失了一個+1的值.因此不要試圖用volatile保證多步操做的原子性,原子性能夠經過synchronized進行維護.多線程

須要注意一點,long類型和double類型的數據長度是64位的,JVM規範容許對於64位類型數據分開賦值,即高位32位和低位32位能夠分開賦值,對於這種狀況可使用volatile修飾保證其賦值是一次完成的.可是!!!雖然JVM是這樣規定的,絕大多數虛擬機仍是實現了64位數據賦值的原子性,即便不使用volatile關鍵字進行修飾也不會出現讀取到只賦值一半的64位類型數據,因此沒必要要每一個longdouble變量以前添加volatile關鍵字.併發

感覺一下volatile

瞭解完原理,來經過一段代碼感覺下volatile.app

public class Volatile implements Runnable{
    //自增變量i
    public /*volatile*/ int i = 0;
    @Override
    public void run() {
        while (true){
            i++; //不斷自增
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Volatile vt = new Volatile();
        Watcher watcher = new Watcher();
        watcher.v = vt;
        Thread t1 = new Thread(vt);
        Thread t2 = new Thread(watcher);
        t1.start();
        t2.start();
        Thread.sleep(10);
        //打印 i  和 s
        System.out.println("Volatile.i = " + vt.i + "\nwatcher.w  = " + watcher.monitor);
        System.exit(0);
    }
}
class Watcher implements Runnable{
    public  Volatile v;

    public  int monitor;
    @Override
    public void run() {
        while (true){
            monitor = v.i;//不斷將v.i的值賦給s
        }
    }
}
// 這是未加volatile修飾的輸出
Volatile.i = 2517483
watcher.w  = 1047805
// 打開volatile註釋的輸出結果
Volatile.i = 332754
watcher.w  = 333354

第一個輸出中未加volatile修飾的i的值和watcher讀取的值相差太遠,ide

第二個輸出中相差就很少了.而且i的值比未加volatile關鍵字的值差不少,說明對volatile變量的賦值消耗會大一些,不過不用在乎,咱們不多對volatile關鍵字進行不斷自增操做,通常都是做爲狀態或者保證對象完整性,並且volatilesynchronized輕量太多了,若是隻爲了保證可見性,volatile必定是最優選.線程

哪些場景使用volatile

狀態變量

因爲boolean的賦值是原子性的,因此volatile布爾變量做爲多線程中止標誌還簡單有效的.code

class Machine{
    volatile boolean stopped = false;

    void stop(){stopped = true;}
}

對象完整發布

這裏要提到單例對象的雙重檢查鎖,對象完整發布也依賴於happens before原則,有興趣能夠本身去查閱,這個原則是比較囉嗦,能夠簡單理解爲我知足happens before,那麼我以前的代碼按順序執行.

public class Singleton {
    //單例對象
    private static Singleton instance = null;
    //私有化構造器,避免外部經過構造器構造對象
    private Singleton(){}
    //這是靜態工廠方法,用來產生對象
    public static Singleton getInstance(){
        if(instance ==null){
        //同步鎖防止屢次new對象
            synchronized (Singleton.class){
            //鎖內非空判斷也是爲了防止建立多個對象
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

這是一個會產生bug的雙重檢查鎖代碼,instance = new Singleton()並非一步完成的,他被分爲這幾步:

1.分配對象空間;
2.初始化對象;
3.設置instance指向剛剛分配的地址。

下面圖中,線程A紅色先得到鎖,B黃色後進入.

這種狀況會出現bug,可是因爲volatile知足happens before原則,因此會等對象實例化以後再對地址賦值,咱們須要將private static Singleton instance = null;改爲private static volatile Singleton instance = null;便可.

其實還有幾種場景,若是想了解更多建議閱讀IBM的技術社區的文章https://www.ibm.com/developerworks/cn/java/j-jtp06197.html

相關文章
相關標籤/搜索