1. 程式人生 > >淺析volatile原理及其使用

淺析volatile原理及其使用

pri 依賴 修飾符 計算 ring 一大步 重點 就是 可見

前言

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


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獲取優先權.其他處理器會被總線禁止,處於阻塞狀態.如下圖:

技術分享圖片

對於第二點,其他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修飾符,需要保證運用場景符合下述規則:

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

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

    volatile boolean shutdownWork;

    public void shutdowm(){
        shutdownWork = true;
    }

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

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


小知識點

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

End

淺析volatile原理及其使用