溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Volatile 關(guān)鍵字淺析

發(fā)布時(shí)間:2020-07-26 03:13:03 來源:網(wǎng)絡(luò) 閱讀:702 作者:wx5c78c8b1dbb1b 欄目:編程語言

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

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方法執(zhí)行結(jié)果
(1)加了volatile關(guān)鍵字的執(zhí)行結(jié)果

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執(zhí)行結(jié)果

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

(3)去掉volatile關(guān)鍵字,換成鎖synchronized的執(zhí)行結(jié)果

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執(zhí)行結(jié)果
(4)使用帶有volatile關(guān)鍵字的v1,調(diào)用getAndIncrement累加到5000

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

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

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)根據(jù)程序次序規(guī)則,1happens-before2;3happens-before4;
(2)根據(jù)volatile規(guī)則,2happens-before3;
(3)根據(jù)happens-before的傳遞規(guī)則,1happens-before4;
我們發(fā)現(xiàn)一個(gè)問題volatile影響了普通的字段,可以理解為write的普通num寫對(duì)read的普通讀num可見了,根據(jù)1happens-before來判斷。
代碼測(cè)試,測(cè)試volatile對(duì)普通變量的影響:
影響特性
(1)任何變量的寫在volatile變量寫之前,那么這個(gè)變量在volatile變量讀之后是可見的.(具體解釋原理第6點(diǎn)中詳解)
5.volatile內(nèi)存語義實(shí)現(xiàn)
1)volatile重排序規(guī)則表
(1)當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile之后。
(2)當(dāng)?shù)谝粋€(gè)是volatile讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile之前。
(3)當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作volatile時(shí),不能進(jìn)行重排序。
2)限制重排序的規(guī)則(內(nèi)存屏障)
(1)在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
(2)在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
(3)在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
(4)在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
上面的內(nèi)存屏障都非常保守,但它可以保證任意處理器平臺(tái),任意程序中都能得到正確的volatile語義。
3)代碼示例分析
(1)volatile寫插入的內(nèi)存屏障
內(nèi)存語義:當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存。
實(shí)現(xiàn)寫語義的內(nèi)存屏障:StoreStore和StoreLoad。如下圖執(zhí)行指令執(zhí)行順序
Volatile 關(guān)鍵字淺析
StoreStore屏障可以保證在volatile寫之前,前面所有的普通寫操作已經(jīng)對(duì)任何處理器可見了。這是因?yàn)镾toreStore屏障將保證上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。
這里比較有意思的是,volatile寫后面的StoreLoad屏障。此屏障的作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序。因?yàn)榫幾g器常無法準(zhǔn)確判斷一個(gè)volatile寫的后面是否需要插入一個(gè)StoreLoad屏障。為了保證能正確實(shí)現(xiàn)volatile的內(nèi)存語義,JMM實(shí)現(xiàn)了保守策略:在每個(gè)volatile寫的后面或者每個(gè)volatile讀前面插入一個(gè)StoreLoad屏障。從整體的執(zhí)行效率角度考慮,JMM最終選擇了在在每個(gè)volatile寫的后面插入一個(gè)StoreLoad屏障。因?yàn)関olatile寫-讀內(nèi)存語義的常見模式是:一個(gè)線程寫volatile變量,讀個(gè)線程讀取volatile讀取同一個(gè)volatile變量。當(dāng)讀線程的數(shù)量大大超過寫線程時(shí),選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。
(2)volatile讀插入的內(nèi)存屏障
Volatile 關(guān)鍵字淺析

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

6、通過內(nèi)存屏障(Memory Barrier)分析volatile實(shí)現(xiàn)
代碼示例:

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讀后面有一個(gè)LoadLoad屏障,它保證了屏障前的Load和屏障后Load不會(huì)重排序;注意Load不保證Load是最新的數(shù)據(jù)(我的理解是因?yàn)镃PU緩存優(yōu)化的原因,參考緩存存儲(chǔ)和無效隊(duì)列),但是LoadLoad屏障保證了只要thread2的vonum為true(不考慮是什么時(shí)間,怎么發(fā)生的),那么屏障后面的num的值一定不會(huì)比vonum這個(gè)版本老。
2)、StoreStore:我們看到在volatile寫的前面插入一個(gè)StoreStore屏障,為什么要插入這個(gè)屏障,我們先來看下StoreStore的作用,保證屏障前的Store不會(huì)和屏障后Store重排序,其實(shí)就是寫入操作的執(zhí)行順序,但是具體什么時(shí)候?qū)懭胍彩遣淮_定的。其實(shí)就是保證了thread1的num的寫入一定先于vonum,為什么要這么做了,其實(shí)是為了保護(hù)volatile的讀語義;假如我們可以重排序,vonum先執(zhí)行,如果此時(shí)thread2執(zhí)行,volatile的vonum已經(jīng)有值,但是num還沒有值,此時(shí)就會(huì)出現(xiàn)問題。但是如果沒有重排序,num一定先于vonum寫入,那么可以保證LoadLoad屏障的語義,vonum為true時(shí),num肯定有值。
3)、LoadStore:在volatile讀的后面也插入LoadStore屏障,結(jié)合上面第5點(diǎn),書上說是禁止下面的普通寫重排序(其實(shí)我想到的是為什么不用禁止volatile寫了,我覺得和StoreLoad屏障有關(guān)系,詳見下面的StoreLoad屏障),這是為什么了,我個(gè)人理解也是為了保證上面LoadLoad屏障的語義,因?yàn)槿绻旅娴钠胀▽懞蜕厦娴膙olatile讀和普通讀重排序,那么我們讀到的普通讀是和volatile讀那個(gè)版本的讀還是普通寫的讀了,肯定是普通寫的讀,那么其實(shí)就破壞了LoadLoad屏障的語義了,普通讀可能是比volatile讀舊的版本,所以要禁止和普通寫的重排序。
4)、StoreLoad:在volatile寫的后面插入StoreLoad屏障,結(jié)合前面的1,2點(diǎn)Store和Load發(fā)生的時(shí)間都是不確定的,而我們知道volatile是保證可見性的。所以StoreLoad的作用是防止屏障前面的所有寫和屏障后面的所有讀重排序,屏障保證了前面的所有Store對(duì)所有處理器可見,屏障保證了后面的所有讀都是前面Store的最新數(shù)據(jù)。其實(shí)StoreLoad屏障就是為了解決1.2點(diǎn)Stoe和Load的時(shí)機(jī)問題,但是它的代價(jià)要比其它幾個(gè)屏障要高(底層是基于lock指令實(shí)現(xiàn)),相當(dāng)于一個(gè)全屏障。
7、總結(jié)
要理解volatile關(guān)鍵字的實(shí)現(xiàn)原理,我覺得先得了解硬件的內(nèi)存模型(順便了解下協(xié)議),然后知道JMM,然后知道JMM為了實(shí)現(xiàn)可見性提供的內(nèi)存屏障:LoadLoad、LoadStore、StoreStore、StoreLoad,它們每一個(gè)是什么意思,組合一起又是什么意思,了解它們的通信機(jī)制,知道volatile關(guān)鍵字的用途。
參考文章:
內(nèi)存屏障(英文)
內(nèi)存屏障(中文)

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI