Volatile 關鍵字淺析

1. volatile的定義
Java編程語言允許線程訪問共享變量,爲了確保共享變量能被準確和一致性地更新,線程應該確保通過排他鎖單獨獲取這個變量。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個字段被聲明成volatile關鍵字,Java線程內存模型確保所有線程看到這個變量值的一致性。
2.volatile的實現原則
1)Lock前綴指令會引起處理器緩存寫回內存。Lock前綴指令導致在執行指令期間,聲言處理器的Lock#信號。在多核處理器環境中,Lock#信號確保在聲言該信號期間,處理器可以獨佔任何共享內存。
2)一個處理器的緩存寫會內存會導致其他處理器的緩存失效。(根據MESI協議
3.volatile的自身特性(自身角度分析特性)
理解volatile特性的一個好方法是把對volatile變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步。如下兩個代碼示例:
volatile關鍵字代碼:

public class VolatileFeaturesExample {

    volatile long v1 = 0L;

    public void set (long v2) {
        this.v1 = v2;
    }

    public long get () {
        return v1;
    }

    public void getAndIncrement() {
        v1++;
    }

    public static void main(String[] args) {
        VolatileFeaturesExample v = new VolatileFeaturesExample();
        /*new Thread(new ThreadSet(v)).start();
        new Thread(new ThreadGet(v)).start();*/
        final CountDownLatch countDownLatch = new CountDownLatch(5000);
        for (int i = 0;i < 5000;i ++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    v.getAndIncrement();
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
            System.out.println(v.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class ThreadGet implements Runnable {
        private VolatileFeaturesExample v;

        public ThreadGet (VolatileFeaturesExample v) {
            this.v = v;
        }

        public void run() {
            long local_v1 = 0L;
            while (local_v1 <10) {
                if (local_v1 != v.get()) {
                    System.out.println("ThreadGet--------------"+v.get());
                    local_v1 = v.get();
                }
            }

        }

    }

    static class ThreadSet implements Runnable {

        private VolatileFeaturesExample v;

        public ThreadSet (VolatileFeaturesExample v) {
            this.v = v;
        }

        public void run() {
            long local_v1 = 0L;
            while (local_v1 < 10) {
                System.out.println("ThreadSet----------"+(++local_v1));
                v.set(local_v1);
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

把volatile改爲鎖synchronized。(這裏只貼了方法代碼,其它和上面一樣)

long v1 = 0L;

    public synchronized void set (long v2) {
        this.v1 = v2;
    }

    public synchronized long get () {
        return v1;
    }

代碼分析:
get和set方法執行結果
(1)加了volatile關鍵字的執行結果

ThreadSet----------1
ThreadGet--------------1
ThreadSet----------2
ThreadGet--------------2
ThreadSet----------3
ThreadGet--------------3
ThreadSet----------4
ThreadGet--------------4
ThreadSet----------5
ThreadGet--------------5
ThreadSet----------6
ThreadGet--------------6
ThreadSet----------7
ThreadGet--------------7
ThreadSet----------8
ThreadGet--------------8
ThreadSet----------9
ThreadGet--------------9
ThreadSet----------10
ThreadGet--------------10

(2)去掉volatile執行結果

ThreadSet----------1
ThreadGet--------------1
ThreadSet----------2
ThreadSet----------3
ThreadSet----------4
ThreadSet----------5
ThreadSet----------6
ThreadSet----------7
ThreadSet----------8
ThreadSet----------9
ThreadSet----------10

(3)去掉volatile關鍵字,換成鎖synchronized的執行結果

ThreadSet----------1
ThreadGet--------------1
ThreadSet----------2
ThreadGet--------------2
ThreadSet----------3
ThreadGet--------------3
ThreadSet----------4
ThreadGet--------------4
ThreadSet----------5
ThreadGet--------------5
ThreadSet----------6
ThreadGet--------------6
ThreadSet----------7
ThreadGet--------------7
ThreadSet----------8
ThreadGet--------------8
ThreadSet----------9
ThreadGet--------------9
ThreadSet----------10
ThreadGet--------------10

getAndIncrement執行結果
(4)使用帶有volatile關鍵字的v1,調用getAndIncrement累加到5000

第一次:5000
第二次:5000
第三次:4999
第四次:5000
第五次:4999

如上面示例代碼所示,一個volatile變量的讀/寫操作,與一個普通變量的讀/寫使用鎖同步,它們之間的執行結果不同。不使用volatile關鍵字,發現一個線程改線,另一個線程可能都不可見。
鎖的happens-before規則保證釋放鎖和獲取鎖的兩個線程之間的內存可見性,這意味着對於一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。
鎖的語義決定了臨界區代碼執行具有原子性。這意味,即使64位的long型和double變量,只要它是volatile變量,對該該變量的讀/寫就具有原子性。根據代碼getAndIncrement方法結果得知,對於volatile++這種複合操作不就有原子性,這些操作本身不具有原子性。
特性
(1)可見性。對於一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量的最後寫入。
(2)原子性。對任意單個volatile變量的讀/寫具有原子性,但類似volatile++這種操作不具有原子性。
4.volatile特性的影響性(從不是volatile變量的角度分析,volatile給它們帶來的內存可見性影響)
1)volatile 寫/讀建立的happens-before關係
其實volatile保證了可見性,其實就是完成了線程之間的通信。
我們來分析下如下代碼的happens-before關係

int num = 0;
    volatile boolean flag = false;

    public void write (int i) {
        num = i;  // 1
        flag = true;// 2
    }

    public  int read () {
        if (flag) { // 3
            int i = num; // 4
            return i;
        }
        return num;
    }

(1)根據程序次序規則,1happens-before2;3happens-before4;
(2)根據volatile規則,2happens-before3;
(3)根據happens-before的傳遞規則,1happens-before4;
我們發現一個問題volatile影響了普通的字段,可以理解爲write的普通num寫對read的普通讀num可見了,根據1happens-before來判斷。
代碼測試,測試volatile對普通變量的影響:
影響特性
(1)任何變量的寫在volatile變量寫之前,那麼這個變量在volatile變量讀之後是可見的.(具體解釋原理第6點中詳解)
5.volatile內存語義實現
1)volatile重排序規則表
(1)當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile之後。
(2)當第一個是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile之前。
(3)當第一個操作是volatile寫,第二個操作volatile時,不能進行重排序。
2)限制重排序的規則(內存屏障)
(1)在每個volatile寫操作的前面插入一個StoreStore屏障。
(2)在每個volatile寫操作的後面插入一個StoreLoad屏障。
(3)在每個volatile讀操作的後面插入一個LoadLoad屏障。
(4)在每個volatile讀操作的後面插入一個LoadStore屏障。
上面的內存屏障都非常保守,但它可以保證任意處理器平臺,任意程序中都能得到正確的volatile語義。
3)代碼示例分析
(1)volatile寫插入的內存屏障
內存語義:當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。
實現寫語義的內存屏障:StoreStore和StoreLoad。如下圖執行指令執行順序
Volatile 關鍵字淺析
StoreStore屏障可以保證在volatile寫之前,前面所有的普通寫操作已經對任何處理器可見了。這是因爲StoreStore屏障將保證上面所有的普通寫在volatile寫之前刷新到主內存。
這裏比較有意思的是,volatile寫後面的StoreLoad屏障。此屏障的作用是避免volatile寫與後面可能有的volatile讀/寫操作重排序。因爲編譯器常無法準確判斷一個volatile寫的後面是否需要插入一個StoreLoad屏障。爲了保證能正確實現volatile的內存語義,JMM實現了保守策略:在每個volatile寫的後面或者每個volatile讀前面插入一個StoreLoad屏障。從整體的執行效率角度考慮,JMM最終選擇了在在每個volatile寫的後面插入一個StoreLoad屏障。因爲volatile寫-讀內存語義的常見模式是:一個線程寫volatile變量,讀個線程讀取volatile讀取同一個volatile變量。當讀線程的數量大大超過寫線程時,選擇在volatile寫之後插入StoreLoad屏障將帶來可觀的執行效率的提升。
(2)volatile讀插入的內存屏障
Volatile 關鍵字淺析

LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。

6、通過內存屏障(Memory Barrier)分析volatile實現
代碼示例:

public class VolatileTest {

    private  boolean vonum = false;
    private  int num = 0;

    // thread1
    public void write (int i) {
        num = i;
        // 插入StoreStore屏障
        vonum = true;
        // 插入StoreLoad屏障
    }

    // thread2
    public void read () {
        if (vonum) {
            // 插入LoadLoad屏障
            // 插入LoadStore屏障
            System.out.println(Thread.currentThread().getName()+"---"+num);
        }
    }

分析:
1)、LoadLoad:在volatile讀後面有一個LoadLoad屏障,它保證了屏障前的Load和屏障後Load不會重排序;注意Load不保證Load是最新的數據(我的理解是因爲CPU緩存優化的原因,參考緩存存儲和無效隊列),但是LoadLoad屏障保證了只要thread2的vonum爲true(不考慮是什麼時間,怎麼發生的),那麼屏障後面的num的值一定不會比vonum這個版本老。
2)、StoreStore:我們看到在volatile寫的前面插入一個StoreStore屏障,爲什麼要插入這個屏障,我們先來看下StoreStore的作用,保證屏障前的Store不會和屏障後Store重排序,其實就是寫入操作的執行順序,但是具體什麼時候寫入也是不確定的。其實就是保證了thread1的num的寫入一定先於vonum,爲什麼要這麼做了,其實是爲了保護volatile的讀語義;假如我們可以重排序,vonum先執行,如果此時thread2執行,volatile的vonum已經有值,但是num還沒有值,此時就會出現問題。但是如果沒有重排序,num一定先於vonum寫入,那麼可以保證LoadLoad屏障的語義,vonum爲true時,num肯定有值。
3)、LoadStore:在volatile讀的後面也插入LoadStore屏障,結合上面第5點,書上說是禁止下面的普通寫重排序(其實我想到的是爲什麼不用禁止volatile寫了,我覺得和StoreLoad屏障有關係,詳見下面的StoreLoad屏障),這是爲什麼了,我個人理解也是爲了保證上面LoadLoad屏障的語義,因爲如果下面的普通寫和上面的volatile讀和普通讀重排序,那麼我們讀到的普通讀是和volatile讀那個版本的讀還是普通寫的讀了,肯定是普通寫的讀,那麼其實就破壞了LoadLoad屏障的語義了,普通讀可能是比volatile讀舊的版本,所以要禁止和普通寫的重排序。
4)、StoreLoad:在volatile寫的後面插入StoreLoad屏障,結合前面的1,2點Store和Load發生的時間都是不確定的,而我們知道volatile是保證可見性的。所以StoreLoad的作用是防止屏障前面的所有寫和屏障後面的所有讀重排序,屏障保證了前面的所有Store對所有處理器可見,屏障保證了後面的所有讀都是前面Store的最新數據。其實StoreLoad屏障就是爲了解決1.2點Stoe和Load的時機問題,但是它的代價要比其它幾個屏障要高(底層是基於lock指令實現),相當於一個全屏障。
7、總結
要理解volatile關鍵字的實現原理,我覺得先得了解硬件的內存模型(順便了解下協議),然後知道JMM,然後知道JMM爲了實現可見性提供的內存屏障:LoadLoad、LoadStore、StoreStore、StoreLoad,它們每一個是什麼意思,組合一起又是什麼意思,瞭解它們的通信機制,知道volatile關鍵字的用途。
參考文章:
內存屏障(英文)
內存屏障(中文)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章