死磕Java——volatile的理解

一北秽、死磕Java——volatile的理解

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

理解volatile的相關(guān)知識(shí)前,先簡(jiǎn)單的認(rèn)識(shí)一下JMM(Java Memory Model),JMMjdk5引入的一種jvm的一種規(guī)范勘高,本身是一種抽象的概念,并不真實(shí)存在绰精,它屏蔽了各種硬件和操作系統(tǒng)的訪問(wèn)差異扛点,它的目的是為了解決由于多線程通過(guò)共享數(shù)據(jù)進(jìn)行通信時(shí),存在的本地內(nèi)存數(shù)據(jù)不一致续镇、編譯器會(huì)對(duì)代碼進(jìn)行指令重排等問(wèn)題美澳。

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

  • 線程解鎖前,必須把共享變量的值刷新回主內(nèi)存摸航;
  • 線程加鎖前制跟,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存中;
  • 加鎖和解鎖使用的是同一把鎖忙厌;

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

image-20190502153724126

說(shuō)明:當(dāng)我們?cè)诔绦蛑?code>new一個(gè)user對(duì)象的時(shí)候凫岖,這個(gè)對(duì)象就存在我們的主內(nèi)存中,當(dāng)多個(gè)線程操作主內(nèi)存的name變量的時(shí)候逢净,會(huì)先將user對(duì)象中的name屬性進(jìn)行拷貝一份到自己線程的工作內(nèi)存中,自己修改自己工作內(nèi)存中的屬性后歼指,再將修改后的屬性值刷新回主內(nèi)存爹土,這就會(huì)存在一些問(wèn)題,例如踩身,一個(gè)線程寫完胀茵,還沒(méi)有寫回到主內(nèi)存,另一個(gè)線程先修改后寫入到主內(nèi)存挟阻,就會(huì)存在數(shù)據(jù)的丟失或者臟數(shù)據(jù)琼娘。所以峭弟,JMM就存在如下規(guī)定:

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

1.2.Volatile關(guān)鍵字

volatilejava虛擬機(jī)提供的一種輕量級(jí)的同步機(jī)制,比較與synchronized脱拼。我們知道的事volatile的三大特性:

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

1.2.1.Volatile如何保證可見性

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

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é)束");
    }
}

這是一個(gè)簡(jiǎn)單的示例程序,存在一個(gè)兩個(gè)線程赌蔑,一個(gè)子線程修改主內(nèi)存的共享數(shù)據(jù)num的值俯在,main線程使用while時(shí)時(shí)檢測(cè)自己是否是道主內(nèi)存的num的值是否被改變,運(yùn)行程序程序執(zhí)行結(jié)束并不會(huì)被打印娃惯,同時(shí)跷乐,程序也不會(huì)停止。這就是線程之間的不可見問(wèn)題趾浅,解決方法就是可以添加volatile關(guān)鍵字劈猿,修改如下:

volatile int num = 0;

1.2.2.Volatile保證可見性的原理

Java程序生成匯編代碼的時(shí)候,我們可以看見潮孽,當(dāng)我們對(duì)添加了volatile關(guān)鍵字修飾的變量時(shí)候揪荣,會(huì)多出一條Lock前綴的的指令。我們知道的是cpu不直接與主內(nèi)存進(jìn)行數(shù)據(jù)交換往史,中間存在一個(gè)高速緩存區(qū)域仗颈,通常是一級(jí)緩存、二級(jí)緩存和三級(jí)緩存椎例,而添加了volatile關(guān)鍵字進(jìn)行操作時(shí)候挨决,生成的Lock前綴的匯編指令主要有以下兩個(gè)作用:

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

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

參考:https://wiki.openjdk.java.net/display/HotSpot/PrintAssembly

在多處理器下,為了保證各個(gè)處理器的緩存是一致的刷晋,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議盖高,每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改眼虱,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài)喻奥,當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里捏悬。

總結(jié):Volatile通過(guò)緩存一致性保證可見性撞蚕。

1.2.3.Volatile不保證原子性

原子性:也可以說(shuō)是保持?jǐn)?shù)據(jù)的完整一致性,也就是說(shuō)當(dāng)某一個(gè)線程操作每一個(gè)業(yè)務(wù)的時(shí)候过牙,不能被其他線程打斷甥厦,不可以被分割操作纺铭,即整體一致性,要么同時(shí)成功刀疙,要么同時(shí)失敗舶赔。

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)時(shí)庙洼,打印的最終結(jié)果幾乎很難為20000顿痪,這就很充分的說(shuō)明了volatile并不能保證數(shù)據(jù)的原子性,這里的num++操作油够,雖然只有一行代碼蚁袭,但是實(shí)際是三步操作,這也是為什么i++在多線程下是非線程安全的石咬。

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

可以參考JMM模型的那一張圖揩悄,就是主內(nèi)存中存在一個(gè)num = 0,當(dāng)其中一個(gè)線程將其修改為1鬼悠,然后將其寫回主內(nèi)存的時(shí)候删性,就被掛起了,另外一個(gè)線程也將主內(nèi)存的num = 0修改為1焕窝,然后寫入后蹬挺,之前的線程被喚醒,快速的寫入主內(nèi)存它掂,覆蓋了已經(jīng)寫入的1巴帮,造成了數(shù)據(jù)丟失操作,兩次操作最終結(jié)果應(yīng)該為2虐秋,但是為1榕茧,這就是為什么會(huì)造成數(shù)據(jù)丟失。再來(lái)看i++對(duì)應(yīng)的字節(jié)碼

image-20190502175617528

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

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

1.2.5.解決Volatile不保證原子性的問(wèn)題

使用AtomicInteger來(lái)保證原子性,有關(guān)AtomicInteger的詳細(xì)知識(shí)抬虽,后面在死磕官觅,官方文檔截圖如下:

image-20190502182016318

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

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的禁止指令重排序

首先,假如寫了如下代碼

carbon

在程序中阐污,我們覺(jué)得是會(huì)依次順序執(zhí)行,但是在計(jì)算機(jī)在執(zhí)行程序的時(shí)候咱圆,為了提高性能笛辟,編譯器和和處理器通常會(huì)對(duì)指令進(jìn)行指令重排序功氨,可能執(zhí)行順序?yàn)椋?—1—3—4,也可能是:1—3—2—4手幢,一般分為下面三種:

image-20190502184808400

雖然處理器會(huì)對(duì)指令進(jìn)行重排捷凄,但是同時(shí)也會(huì)遵守一些規(guī)則,例如上述代碼不可能重排后將第四句代碼第一個(gè)執(zhí)行围来,所以跺涤,單線程下確保程序的最終執(zhí)行結(jié)果和順序執(zhí)行結(jié)一致,這就是處理器在進(jìn)行指令重排序時(shí)候必須考慮的就是指令之間的數(shù)據(jù)依賴性监透。

但是桶错,在多線程環(huán)境下,由于編譯器重排的存在胀蛮,兩個(gè)線程使用的變量能否保證一致性無(wú)法確定院刁,所以結(jié)果就無(wú)法一致。在看一個(gè)示例:

http://image.luokangyuan.com/2019-05-02-113323.png

在多線程環(huán)境下粪狼,第一種就是順序執(zhí)行init方法退腥,先將num進(jìn)行賦值操作,在執(zhí)行update方法再榄,結(jié)果:num為6狡刘,但是存在編譯器重排,那么可能先執(zhí)行falg = true;再執(zhí)行num = 1;困鸥,最終num為5嗅蔬;

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

前面說(shuō)到了volatile禁止指令重排優(yōu)化,從而避免在多線程環(huán)境下出現(xiàn)結(jié)果錯(cuò)亂的現(xiàn)象窝革。這是因?yàn)樵?code>volatile會(huì)在指令之間插入一條內(nèi)存屏障指令购城,通過(guò)內(nèi)存屏障指令告訴CPU和編譯器不管什么指令,都不進(jìn)行指令重新排序虐译。也就說(shuō)說(shuō)通過(guò)插入的內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行指令重新排序優(yōu)化瘪板。

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

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

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

將上述代碼修改為:

volatile int num = 0;

volatile boolean falg = false;

這樣就保證執(zhí)行init方法的時(shí)候一定是先執(zhí)行num = 1;再執(zhí)行falg = true;,就避免的了結(jié)果出錯(cuò)的現(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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末兰英,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子供鸠,更是在濱河造成了極大的恐慌畦贸,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異薄坏,居然都是意外死亡趋厉,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門胶坠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)君账,“玉大人,你說(shuō)我怎么就攤上這事沈善∠缡” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵闻牡,是天一觀的道長(zhǎng)净赴。 經(jīng)常有香客問(wèn)我,道長(zhǎng)澈侠,這世上最難降的妖魔是什么劫侧? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮哨啃,結(jié)果婚禮上烧栋,老公的妹妹穿的比我還像新娘。我一直安慰自己拳球,他們只是感情好审姓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著祝峻,像睡著了一般魔吐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上莱找,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天酬姆,我揣著相機(jī)與錄音,去河邊找鬼奥溺。 笑死辞色,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浮定。 我是一名探鬼主播相满,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼桦卒!你這毒婦竟也來(lái)了立美?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤方灾,失蹤者是張志新(化名)和其女友劉穎建蹄,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡躲撰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年针贬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了击费。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拢蛋。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蔫巩,靈堂內(nèi)的尸體忽然破棺而出谆棱,到底是詐尸還是另有隱情,我是刑警寧澤圆仔,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布垃瞧,位于F島的核電站,受9級(jí)特大地震影響坪郭,放射性物質(zhì)發(fā)生泄漏个从。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一歪沃、第九天 我趴在偏房一處隱蔽的房頂上張望嗦锐。 院中可真熱鬧,春花似錦沪曙、人聲如沸奕污。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)碳默。三九已至,卻和暖如春缘眶,著一層夾襖步出監(jiān)牢的瞬間嘱根,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工巷懈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留该抒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓砸喻,卻偏偏與公主長(zhǎng)得像柔逼,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子割岛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

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