淺析volatile原理及其使用

mars_jun發表於2018-11-20

前言

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


volatile的定義及其實現

定義:如果一個欄位被宣告成volatile,那麼java執行緒記憶體模型將確保所有執行緒看到的這個變數的值都是一致的.

從它的定義當中我們們也可以瞭解到volatile具有可見性的特性.但它具體是如何保證其可見性的呢?

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

//Java程式碼如下
instance = new Singleton(); //這裡instance是volatile變數
//反彙編後
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock add1 $0x0,(%esp);
複製程式碼

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

  1. 立即回寫當前處理器快取行的值到記憶體.
  2. 其他所有cpu快取了該地址的資料將會失效.

這裡大家也許會有疑問,有沒有可能存在多個cpu一起回寫資料?

答案是不會的.雖然cpu鼓勵多個處理器可以有競爭,但是匯流排會對競爭做出裁決,只會有一個cpu獲取優先權.其他處理器會被匯流排禁止,處於阻塞狀態.如下圖:

淺析volatile原理及其使用

對於第二點,其他cpu快取該地址的資料失效後想要再次使用的話就必須得從主記憶體中重新讀取,這樣就能保證再次執行計算時所獲取的值是最新的,也可以認為所有CPU的快取是一致的,這也就證明了volatile修飾的欄位是可見的.


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

這裡我們們先引進一段程式碼:

/**
 * 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,但經過測試發現,每次的輸出結果都會不一樣.但具體是什麼原因造成的?

其實最主要的問題是出在increase()這個自增方法上,這個操作不是一個原子操作,也就是不是一步就能操作完成的,其中會經歷count值入棧,add,出棧,到操作執行緒快取,最終到記憶體等等一系列步驟.當A執行緒其執行這些指令時,B執行緒正好將資料同步到了主記憶體中,此時A執行緒棧頂的資料就會變成過期資料,然後A執行緒就會將較小的值同步到主記憶體中.

淺析volatile原理及其使用


如何正確的運用volatile

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

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

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

volatile boolean shutdownWork;

    public void shutdowm(){
        shutdownWork = true;
    }

    public void doWork(){
        while (!shutdownWork){
            //execute task
        }
    }
複製程式碼

上面這段程式碼執行結果並無需依賴shutdownWork的值,但是隻要shutdownWork的值一旦經過改變,便會立即被其他所有執行緒所感知,然後停止執行任務.


小知識點

在多處理器下,為了保證各個處理器的快取是一致的,處理器會使用嗅探技術來保證它的內部快取,系統記憶體和其他處理器的快取的資料在匯流排上保持一致.如果通過嗅探檢測到其他處理器打算寫記憶體地址,而這個地址當前處於共享狀態,那麼正在嗅探的處理器將使它的快取無效,在下次訪問相同的記憶體地址時,強制執行快取行填充,也就是從記憶體中重新讀取該記憶體地址指向的值.


End

相關文章