淺析volatile原理及其使用

前言

常常在網上看一些大牛們的博客,從中收穫到一些東西的同時會產生一種崇拜感,從而萌發了本身寫寫博客的念頭.然而已經有這個念頭好久,卻始終不敢下手開始寫.今天算是邁出了人生的一大步^_^!java


volatile的定義及其實現

定義:若是一個字段被聲明成volatile,那麼java線程內存模型將確保全部線程看到的這個變量的值都是一致的.緩存

從它的定義當中我們也能夠了解到volatile具備可見性的特性.但它具體是如何保證其可見性的呢?安全

先看一段JIT編譯器生成的彙編指令併發

//Java代碼以下
instance = new Singleton(); //這裏instance是volatile變量
//反彙編後
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock add1 $0x0,(%esp);

有volatile修飾的變量在進行寫操做時會出現第二行反彙編代碼,重點在lock這個指令.它有兩個目的:ide

  1. 當即回寫當前處理器緩存行的值到內存.
  2. 其餘全部cpu緩存了該地址的數據將會失效.

這裏你們也許會有疑問,有沒有可能存在多個cpu一塊兒回寫數據?測試

答案是不會的.雖然cpu鼓勵多個處理器能夠有競爭,可是總線會對競爭作出裁決,只會有一個cpu獲取優先權.其餘處理器會被總線禁止,處於阻塞狀態.以下圖:線程

對於第二點,其餘cpu緩存該地址的數據失效後想要再次使用的話就必須得從主內存中從新讀取,這樣就能保證再次執行計算時所獲取的值是最新的,也能夠認爲全部CPU的緩存是一致的,這也就證實了volatile修飾的字段是可見的.3d


可見性不表明在併發下是安全的

這裏我們先引進一段代碼:code

/**
 * volatile 變量自增運算
 *
 * @author mars
 */
public class VolatileTest {
    public static volatile int count = 0;

    public static void increase() {
        count++;
    }

    private static final int THREAD_COUNTS = 20;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(THREAD_COUNTS);
        Thread[] threads = new Thread[THREAD_COUNTS];
        for (int j = 0; j < THREAD_COUNTS; j++) {
            threads[j] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                    latch.countDown();
                }
            });
            threads[j].start();
        }
        //等待全部的線程執行結束
        latch.await();

        System.out.println(count);
    }
}

這段代碼供發起了20個線程,對count變量進行了10000次自增操做,若是volatile修飾的字段在併發下是安全的話,講道理最終結果都會是200000,但通過測試發現,每次的輸出結果都會不同.但具體是什麼緣由形成的?blog

其實最主要的問題是出在increase()這個自增方法上,這個操做不是一個原子操做,也就是否是一步就能操做完成的,其中會經歷count值入棧,add,出棧,到操做線程緩存,最終到內存等等一系列步驟.當A線程其執行這些指令時,B線程正好將數據同步到了主內存中,此時A線
程棧頂的數據就會變成過時數據,而後A線程就會將較小的值同步到主內存中.


如何正確的運用volatile

要想運用好volatile修飾符,須要保證運用場景符合下述規則:

  1. 運算結果不依賴變量的當前值.
  2. 該變量不須要和其餘變量共同參與約束.

例如使用volatile變量來控制併發就很合適:

volatile boolean shutdownWork;

    public void shutdowm(){
        shutdownWork = true;
    }

    public void doWork(){
        while (!shutdownWork){
            //execute task
        }
    }

上面這段代碼運行結果並沒有需依賴shutdownWork的值,可是隻要shutdownWork的值一旦通過改變,便會當即被其餘全部線程所感知,而後中止執行任務.


小知識點

在多處理器下,爲了保證各個處理器的緩存是一致的,處理器會使用嗅探技術來保證它的內部緩存,系統內存和其餘處理器的緩存的數據在總線上保持一致.若是經過嗅探一個處理器來檢測其餘處理器打算寫內存地址,而這個地址當前處於共享狀態,那麼正在嗅探的處理器將使它的緩存無效,在下次訪問相同的內存地址時,強制執行緩存行填充,也就是從內存中從新讀取該內存地址指向的值.

End

相關文章
相關標籤/搜索