死磕Java——volatile的理解

一、死磕Java——volatile的理解

1.1.JMM內(nèi)存模型

理解volatile的相關(guān)知識前改化,先簡單的認(rèn)識一下JMM(Java Memory Model),JMM是jdk5引入的一種jvm的一種規(guī)范佩憾,本身是一種抽象的概念哮伟,并不真實(shí)存在干花,它屏蔽了各種硬件和操作系統(tǒng)的訪問差異,它的目的是為了解決由于多線程通過共享數(shù)據(jù)進(jìn)行通信時楞黄,存在的本地內(nèi)存數(shù)據(jù)不一致池凄、編譯器會對代碼進(jìn)行指令重排等問題。

JMM有關(guān)同步的規(guī)定:

  • 線程解鎖前鬼廓,必須把共享變量的值刷新回主內(nèi)存肿仑;
  • 線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存中碎税;
  • 加鎖和解鎖使用的是同一把鎖尤慰;

關(guān)于上述規(guī)定如下圖解:

說明:當(dāng)我們在程序中new一個user對象的時候,這個對象就存在我們的主內(nèi)存中雷蹂,當(dāng)多個線程操作主內(nèi)存的name變量的時候伟端,會先將user對象中的name屬性進(jìn)行拷貝一份到自己線程的工作內(nèi)存中,自己修改自己工作內(nèi)存中的屬性后匪煌,再將修改后的屬性值刷新回主內(nèi)存责蝠,這就會存在一些問題,例如萎庭,一個線程寫完霜医,還沒有寫回到主內(nèi)存,另一個線程先修改后寫入到主內(nèi)存驳规,就會存在數(shù)據(jù)的丟失或者臟數(shù)據(jù)支子。所以,JMM就存在如下規(guī)定:

  • 可見性
  • 原子性
  • 有序性

1.2.Volatile關(guān)鍵字

volatile是java虛擬機(jī)提供的一種輕量級的同步機(jī)制达舒,比較與synchronized值朋。我們知道的事volatile的三大特性:

  • 可見性
  • 不保證原子性
  • 禁止指令重排

1.2.1.Volatile如何保證可見性

可見性就是當(dāng)多個線程操作主內(nèi)存的共享數(shù)據(jù)的時候,當(dāng)其中一個線程修改了數(shù)據(jù)寫回主內(nèi)存的時候巩搏,回立刻通知其他線程昨登,這就是線程的可見性。先看一個簡單的例子:

class MyDataDemo {
    int num = 0;

    public void updateNum() {
        this.num = 60;
    }
}

public class VolatileDemo {

    public static void main(String[] args) {

        MyDataDemo myData = new MyDataDemo();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.updateNum();
            System.out.println("num的值:" + myData.num);
        }, "子線程").start();

        while (myData.num == 0) {}
        System.out.println("程序執(zhí)行結(jié)束");
    }
}

這是一個簡單的示例程序贯底,存在一個兩個線程丰辣,一個子線程修改主內(nèi)存的共享數(shù)據(jù)num的值,main線程使用while時時檢測自己是否是道主內(nèi)存的num的值是否被改變禽捆,運(yùn)行程序程序執(zhí)行結(jié)束并不會被打印笙什,同時,程序也不會停止胚想。這就是線程之間的不可見問題琐凭,解決方法就是可以添加volatile關(guān)鍵字,修改如下:

volatile int num = 0;

1.2.2.Volatile保證可見性的原理

將Java程序生成匯編代碼的時候浊服,我們可以看見统屈,當(dāng)我們對添加了volatile關(guān)鍵字修飾的變量時候胚吁,會多出一條Lock前綴的的指令。我們知道的是cpu不直接與主內(nèi)存進(jìn)行數(shù)據(jù)交換愁憔,中間存在一個高速緩存區(qū)域腕扶,通常是一級緩存、二級緩存和三級緩存吨掌,而添加了volatile關(guān)鍵字進(jìn)行操作時候半抱,生成的Lock前綴的匯編指令主要有以下兩個作用:

  • 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存;
  • 這個寫回內(nèi)存的操作會使得其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效膜宋;

Idea查看程序的匯編指令在VM啟動參數(shù)配上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly即可代虾;

在多處理器下,為了保證各個處理器的緩存是一致的激蹲,就會實(shí)現(xiàn)緩存一致性協(xié)議棉磨,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改学辱,就會將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài)乘瓤,當(dāng)處理器對這個數(shù)據(jù)進(jìn)行修改操作的時候,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里策泣。

總結(jié):Volatile通過緩存一致性保證可見性衙傀。

1.2.3.Volatile不保證原子性

原子性:也可以說是保持?jǐn)?shù)據(jù)的完整一致性,也就是說當(dāng)某一個線程操作每一個業(yè)務(wù)的時候萨咕,不能被其他線程打斷统抬,不可以被分割操作,即整體一致性危队,要么同時成功聪建,要么同時失敗。

class MyDataDemo {
    volatile int num = 0;

    public void addNum() {
        num++;
    }
}
public class VolatileDemo {

    public static void main(String[] args) {
        MyDataDemo data = new MyDataDemo();
        for(int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j < 1000; j++) {
                    data.addNum();
                }
            }, "當(dāng)前子線程為線程" + String.valueOf(i)).start();
        }
        // 等待所有線程執(zhí)行結(jié)束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最終結(jié)果:" + data.num);
    }
}

上述代碼就是在共享數(shù)據(jù)前添加了volatile關(guān)鍵字茫陆,當(dāng)時金麸,打印的最終結(jié)果幾乎很難為20000,這就很充分的說明了volatile并不能保證數(shù)據(jù)的原子性簿盅,這里的num++操作挥下,雖然只有一行代碼,但是實(shí)際是三步操作桨醋,這也是為什么i++在多線程下是非線程安全的棚瘟。

1.2.4.為什么Volatile不保證原子性

可以參考JMM模型的那一張圖,就是主內(nèi)存中存在一個num = 0喜最,當(dāng)其中一個線程將其修改為1偎蘸,然后將其寫回主內(nèi)存的時候,就被掛起了,另外一個線程也將主內(nèi)存的num = 0修改為1禀苦,然后寫入后,之前的線程被喚醒遂鹊,快速的寫入主內(nèi)存振乏,覆蓋了已經(jīng)寫入的1,造成了數(shù)據(jù)丟失操作秉扑,兩次操作最終結(jié)果應(yīng)該為2慧邮,但是為1,這就是為什么會造成數(shù)據(jù)丟失舟陆。再來看i++對應(yīng)的字節(jié)碼


簡單翻譯一下字節(jié)碼的操作:

  • aload_0:從局部變量表的相應(yīng)位置裝載一個對象引用到操作數(shù)棧的棧頂误澳;
  • dup:復(fù)制棧頂元素;
  • getfield:先獲得原始值秦躯;
  • iadd:進(jìn)行+1操作忆谓;
  • putfield:再把累加后的值寫回主內(nèi)存操作;

1.2.5.解決Volatile不保證原子性的問題

使用AtomicInteger來保證原子性踱承,有關(guān)AtomicInteger的詳細(xì)知識倡缠,后面在死磕,官方文檔截圖如下:

修改之前的不保證原子性的代碼如下:

class MyDataDemo {
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomicInteger() {
        atomicInteger.getAndIncrement();
    }
}
public class VolatileDemo {

    public static void main(String[] args) {
        MyDataDemo data = new MyDataDemo();
        for(int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    data.addAtomicInteger();
                }
            }, "當(dāng)前子線程為線程" + String.valueOf(i)).start();
        }
        // 等待所有線程執(zhí)行結(jié)束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最終結(jié)果:" + data.atomicInteger);
    }
}

1.2.6.Volatile的禁止指令重排序

在程序中茎活,我們覺得是會依次順序執(zhí)行昙沦,但是在計算機(jī)在執(zhí)行程序的時候,為了提高性能载荔,編譯器和和處理器通常會對指令進(jìn)行指令重排序盾饮,可能執(zhí)行順序?yàn)椋?—1—3—4,也可能是:1—3—2—4懒熙,一般分為下面三種:


雖然處理器會對指令進(jìn)行重排丘损,但是同時也會遵守一些規(guī)則,例如上述代碼不可能重排后將第四句代碼第一個執(zhí)行工扎,所以号俐,單線程下確保程序的最終執(zhí)行結(jié)果和順序執(zhí)行結(jié)一致,這就是處理器在進(jìn)行指令重排序時候必須考慮的就是指令之間的數(shù)據(jù)依賴性定庵。
但是吏饿,在多線程環(huán)境下,由于編譯器重排的存在蔬浙,兩個線程使用的變量能否保證一致性無法確定猪落,所以結(jié)果就無法一致。在看一個示例:


在多線程環(huán)境下畴博,第一種就是順序執(zhí)行init方法笨忌,先將num進(jìn)行賦值操作,在執(zhí)行update方法俱病,結(jié)果:num為6官疲,但是存在編譯器重排袱结,那么可能先執(zhí)行falg = true;再執(zhí)行num = 1;,最終num為5途凫;

1.2.7.Volatile禁止指令重排序的原理

前面說到了volatile禁止指令重排優(yōu)化垢夹,從而避免在多線程環(huán)境下出現(xiàn)結(jié)果錯亂的現(xiàn)象。這是因?yàn)樵趘olatile會在指令之間插入一條內(nèi)存屏障指令维费,通過內(nèi)存屏障指令告訴CPU和編譯器不管什么指令果元,都不進(jìn)行指令重新排序。也就說說通過插入的內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行指令重新排序優(yōu)化犀盟。

什么是內(nèi)存屏障

內(nèi)存屏障是一個CPU指令而晒,他的作用有兩個:

  • 保證特定操作的執(zhí)行順序;
  • 保證某些變量的內(nèi)存可見性阅畴;

將上述代碼修改為:

volatile int num = 0;

volatile boolean falg = false;

這樣就保證執(zhí)行init方法的時候一定是先執(zhí)行num = 1;再執(zhí)行falg = true;倡怎,就避免的了結(jié)果出錯的現(xiàn)象。

1.3.Volatile的單例模式

public class SingletonDemo {

    private static volatile SingletonDemo instance = null;

    private SingletonDemo(){};

    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贱枣,一起剝皮案震驚了整個濱河市诈胜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌冯事,老刑警劉巖焦匈,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異昵仅,居然都是意外死亡缓熟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門摔笤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來够滑,“玉大人,你說我怎么就攤上這事吕世≌么ィ” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵命辖,是天一觀的道長况毅。 經(jīng)常有香客問我,道長尔艇,這世上最難降的妖魔是什么尔许? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮终娃,結(jié)果婚禮上味廊,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好余佛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布柠新。 她就那樣靜靜地躺著,像睡著了一般辉巡。 火紅的嫁衣襯著肌膚如雪恨憎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天红氯,我揣著相機(jī)與錄音框咙,去河邊找鬼咕痛。 笑死痢甘,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的茉贡。 我是一名探鬼主播塞栅,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼腔丧!你這毒婦竟也來了放椰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤愉粤,失蹤者是張志新(化名)和其女友劉穎砾医,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體衣厘,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡如蚜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了影暴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片错邦。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖型宙,靈堂內(nèi)的尸體忽然破棺而出撬呢,到底是詐尸還是另有隱情,我是刑警寧澤妆兑,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布魂拦,位于F島的核電站,受9級特大地震影響搁嗓,放射性物質(zhì)發(fā)生泄漏晨另。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一谱姓、第九天 我趴在偏房一處隱蔽的房頂上張望借尿。 院中可真熱鬧,春花似錦、人聲如沸路翻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茂契。三九已至蝶桶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掉冶,已是汗流浹背真竖。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厌小,地道東北人恢共。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像璧亚,于是被迫代替她去往敵國和親讨韭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容