希望是volatile的最后一次理解

第一次理解:

剛學(xué)java時(shí)帅霜,對(duì)于volatile的記憶就是:

  • volatile保證可見(jiàn)性
  • volatile防止指令重排序
  • volatile不保證原子性

沒(méi)過(guò)腦的背了一下,寫(xiě)代碼的時(shí)候也沒(méi)用到過(guò)惶看,以為不重要,然后就不了了之。

第二次理解

一段代碼引起好奇

class Singleton{
    private volatile static Singleton instance = null;
     
    private Singleton() {
         
    }
     
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

上圖為比較經(jīng)典的dcl(dubbo check lock)單例模式政基,雙重if判斷是為了防止多線程多次創(chuàng)建,但是instance屬性為什么還要加個(gè)volatile關(guān)鍵字呢闹啦,有什么作用么沮明?
其實(shí)它的作用主要體現(xiàn)在禁止指令重排序

首先先理解下什么叫指令重排序窍奋?
指令重排序可以說(shuō)是jvm對(duì)程序執(zhí)行的一個(gè)優(yōu)化珊擂,他可以保證普通的變量在方法執(zhí)行的過(guò)程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中寫(xiě)的順序保持一致费变。如

x = 1;
y = 2;

這兩條賦值語(yǔ)句之間沒(méi)有依賴關(guān)系摧扇,所以在具體執(zhí)行時(shí)可能會(huì)先賦值y在賦值x,發(fā)生了指令重排挚歧。
而上述DCL代碼中雖然表面只有這instance = new Singleton();一條語(yǔ)句扛稽,但是這個(gè)賦值操作編譯成字節(jié)碼文件后是分為3個(gè)步驟來(lái)完成的:

  1. 為對(duì)象開(kāi)辟內(nèi)存空間并賦默認(rèn)值
  2. 調(diào)用構(gòu)造函數(shù)為對(duì)象賦初始值
  3. 將instance引用指向剛開(kāi)辟的內(nèi)存地址

而程序在執(zhí)行這三步時(shí),會(huì)有可能先執(zhí)行3再執(zhí)行2滑负,如果發(fā)生這種情況在张,線程一先將引用指向地址,還沒(méi)來(lái)得及執(zhí)行構(gòu)造方法矮慕,線程二進(jìn)來(lái)判斷instance帮匾!=null 直接拿這半初始化的對(duì)象去使用,就出現(xiàn)了問(wèn)題痴鳄。
所以此處需要用volatile關(guān)鍵字來(lái)修飾變量瘟斜,禁止指令重排序情況的發(fā)生。那么volatile是如何做到禁止重排序的呢?
《深入理解java虛擬機(jī)》中這樣寫(xiě)道:

我們對(duì)volatile修飾的變量進(jìn)行編譯后發(fā)現(xiàn)螺句,在賦值操作后多執(zhí)行了一個(gè)“l(fā)ock addl $0x0,(%esp)”,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障(Memory Barrier 或 Memory Fence虽惭,指重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置)

也有別的博主這樣寫(xiě)道:

JMM為volatile加內(nèi)存屏障有以下4種情況:
在每個(gè)volatile寫(xiě)操作的前面插入一個(gè)StoreStore屏障,防止寫(xiě)volatile與后面的寫(xiě)操作重排序蛇尚。
在每個(gè)volatile寫(xiě)操作的后面插入一個(gè)StoreLoad屏障芽唇,防止寫(xiě)volatile與后面的讀操作重排序。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障取劫,防止讀volatile與后面的讀操作重排序匆笤。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障,防止讀volatile與后面的寫(xiě)操作重排序谱邪。

第三次理解

那么保證可見(jiàn)性又是指什么東東疚膊?
要想理解這可見(jiàn)性,需要先了解java內(nèi)存模型(jmm)虾标。學(xué)過(guò)計(jì)算機(jī)的同學(xué)都知道多核cpu中每個(gè)cpu都有自己的高速緩存寓盗,如L1,L2,L3,且每個(gè)cpu之間的緩存是隔離的璧函,即數(shù)據(jù)不可見(jiàn)傀蚌。而多個(gè)cpu又共享一個(gè)主內(nèi)存,數(shù)據(jù)一般會(huì)從磁盤(pán)讀取到主內(nèi)存當(dāng)中蘸吓,當(dāng)cpu需要處理數(shù)據(jù)時(shí)善炫,需要從主內(nèi)存讀取數(shù)據(jù)到自己的緩存當(dāng)中然后進(jìn)行運(yùn)算,運(yùn)算結(jié)束后將最新數(shù)據(jù)同步回內(nèi)存之中库继。當(dāng)然這種模型也伴隨這緩存一致性問(wèn)題的出現(xiàn)箩艺。
其實(shí)java內(nèi)存模型和cpu模型非常的類(lèi)似:
每個(gè)線程擁有自己的工作內(nèi)存,然后共享的變量會(huì)存放在主內(nèi)存(jvm的內(nèi)存)當(dāng)中宪萄,線程之間工作內(nèi)存互相隔離艺谆。如圖:


image.png

上圖來(lái)源于《深入理解java虛擬機(jī)363頁(yè)》

我們?cè)賮?lái)看個(gè)容易理解的圖:


image.png

再回到我們的保證可見(jiàn)性的探討:
如上圖所示,若線程A和B都操作主內(nèi)存的共享變量時(shí)拜英,AB會(huì)將共享變量先拷貝會(huì)自己的工作內(nèi)存静汤,在A率先完成修改完之后再同步刷回到主內(nèi)存當(dāng)中,此時(shí)線程B本地內(nèi)存的數(shù)據(jù)還是最先拷貝的舊數(shù)據(jù)居凶,沒(méi)有及時(shí)的獲取到已修改的最新數(shù)據(jù)虫给,最后會(huì)造成數(shù)據(jù)不一致問(wèn)題。
而volatile修飾變量時(shí)侠碧,它會(huì)保證修改的值會(huì)立即被更新到主存抹估,并通知其他線程當(dāng)前緩存的變量已失效,需要重新到主內(nèi)存中讀取弄兜。
底層也是通過(guò)內(nèi)存屏障來(lái)保證的药蜻。
針對(duì)這個(gè)特性瓷式,常見(jiàn)的使用的場(chǎng)景為狀態(tài)標(biāo)記量

public class VolatileTest1 {
    volatile static boolean flag = false;

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("t1 start");
            while (!flag){
                System.out.println("doing something");
            }
        },"t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
    }
}

和我們期望的一樣,1秒后谷暮,程序正常停止。
但是好奇的我開(kāi)始思考盛垦,那是不是只要不加volatile湿弦,程序就不會(huì)停止?立即更新的反義詞是什么腾夯?正常情況下颊埃,線程會(huì)不會(huì),什么時(shí)候會(huì)把修改的值寫(xiě)會(huì)主內(nèi)存蝶俱,別的線程又會(huì)什么時(shí)候會(huì)去重新讀劝嗬?
帶著好奇我把上訴代碼中的volatile去掉榨呆,運(yùn)行結(jié)果如圖:

image.png

沒(méi)錯(cuò) 程序居然正常停掉了罗标!
然后我又把while循環(huán)里的system輸出去掉后,再次運(yùn)行:
image.png

這次又沒(méi)有停止;摺闯割!
難道就是因?yàn)橐痪漭敵稣Z(yǔ)句的問(wèn)題么?我又嘗試換成i++試試:
image.png

這次也沒(méi)有停止8筒稹V胬!
很神奇丙笋,搞得我也很懵逼P怀骸!御板!
我不知道是不是因?yàn)榄h(huán)境的原因锥忿,我用的jdk11和8,idea2020.1.2怠肋,
個(gè)人初步猜測(cè):不加volatile缎谷,即正常情況下,本地線程更新值后灶似,會(huì)很快的寫(xiě)回主內(nèi)存列林,而其他線程什么時(shí)候重新從主內(nèi)存中讀取是不確定的。
上述while代碼里面執(zhí)行點(diǎn)稍微費(fèi)時(shí)的操作(如輸出酪惭,sleep 1s)希痴,都是可以停止的,如果循壞太快春感,它可能沒(méi)時(shí)間去重新讀取flag的值砌创。
(希望有大佬看到小弟的這篇文章虏缸,并指點(diǎn)一二。)

第四次理解

那不保證原子性又是什么鬼嫩实?
原子性:保證指令不會(huì)受到線程上下文切換的影響刽辙,即一個(gè)操作不會(huì)被cpu切換所打斷。
我們舉一個(gè)最常見(jiàn)的案列來(lái)說(shuō)明:
多個(gè)線程對(duì)同一個(gè)數(shù)字進(jìn)行++操作:

public class VolatileDemo {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final VolatileDemo test = new VolatileDemo();
        System.out.println("start");

        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    test.increase();
                }
            }).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(test.inc);
    }
}

我們啟動(dòng)了20個(gè)線程對(duì)inc進(jìn)行++操作甲献,每個(gè)線程+10000宰缤,理想結(jié)果應(yīng)該為200000,但是實(shí)際運(yùn)行結(jié)果卻小于這個(gè)值晃洒,而且結(jié)果每次都不一樣(可以多運(yùn)行幾次觀察):


不保證原子性.png

這是為什么呢慨灭,程序中inc已經(jīng)加了volatile修飾,保證了線程的可見(jiàn)性球及,但是為什么結(jié)果還是會(huì)比預(yù)想的小呢氧骤?
這是因?yàn)?+操作并不是簡(jiǎn)單的一步操作,即他不是原子性的吃引,查看編譯后的字節(jié)碼文件筹陵,++的實(shí)際操作為:
(實(shí)事求是地說(shuō),使用字節(jié)碼來(lái)分析并發(fā)問(wèn)題仍然是不嚴(yán)謹(jǐn)?shù)哪鞒撸驗(yàn)榧词咕幾g出來(lái)只有一條字節(jié)碼指令惶翻,也并不意味執(zhí)行這條指令就是一個(gè)原子操作。一條字節(jié)碼指令在解釋執(zhí)行時(shí)鹅心,解釋器要運(yùn) 行許多行代碼才能實(shí)現(xiàn)它的語(yǔ)義吕粗。如果是編譯執(zhí)行,一條字節(jié)碼指令也可能轉(zhuǎn)化成若干條本地機(jī)器碼 指令旭愧。)

 public void increase();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field inc:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field inc:I
      10: return

inc++操作分成了2.獲取字段 5.準(zhǔn)備常數(shù)1 6.進(jìn)行加1操作 7.賦值 四步
不保證原子性颅筋,即無(wú)法確保這四步操作不會(huì)被cpu切換打斷:


image.png

如圖cpu在線程1修改完之后還未寫(xiě)入內(nèi)存時(shí),切換到線程2输枯,執(zhí)行完了++操作议泵,此時(shí)cpu切換回線程1又把inc=1 寫(xiě)回去,造成了inc的值被覆蓋桃熄。
我們?cè)倏聪缕胀ǖ馁x值操作的字節(jié)碼文件 如:x=1

public void fun1(){
        inc = 1;
    } 
// 編譯后
 public void fun1();
    Code:
       0: aload_0
       1: iconst_1
       2: putfield      #2                  // Field inc:I
       5: return

他沒(méi)有g(shù)etfield和add的操作先口,直接賦值,所以賦值操作算是原子性的瞳收。

而synchronized是如何保證原子性的呢碉京?
通過(guò)字節(jié)碼文件我們可以發(fā)現(xiàn),用synchronized修飾真的代碼塊在前后會(huì)執(zhí)行monitorenter和monitorexit指令螟深,這minitor指令底層則是通過(guò)lock和unlock來(lái)滿足原子性的谐宙,他只允許同時(shí)只有一個(gè)線程來(lái)操作資源。

推薦一篇很詳細(xì)很全面的文章界弧,此篇部分文字也有參考如下文章:

https://www.cnblogs.com/bangiao/p/13195668.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凡蜻,一起剝皮案震驚了整個(gè)濱河市搭综,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌划栓,老刑警劉巖兑巾,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異忠荞,居然都是意外死亡蒋歌,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)钻洒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)奋姿,“玉大人锄开,你說(shuō)我怎么就攤上這事素标。” “怎么了萍悴?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵头遭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我癣诱,道長(zhǎng)计维,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任撕予,我火速辦了婚禮鲫惶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘实抡。我一直安慰自己欠母,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布吆寨。 她就那樣靜靜地躺著赏淌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪啄清。 梳的紋絲不亂的頭發(fā)上六水,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音辣卒,去河邊找鬼掷贾。 笑死,一個(gè)胖子當(dāng)著我的面吹牛荣茫,可吹牛的內(nèi)容都是我干的胯盯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼计露,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼博脑!你這毒婦竟也來(lái)了憎乙?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤叉趣,失蹤者是張志新(化名)和其女友劉穎泞边,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體疗杉,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阵谚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了烟具。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梢什。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖朝聋,靈堂內(nèi)的尸體忽然破棺而出嗡午,到底是詐尸還是另有隱情,我是刑警寧澤冀痕,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布荔睹,位于F島的核電站,受9級(jí)特大地震影響言蛇,放射性物質(zhì)發(fā)生泄漏僻他。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一腊尚、第九天 我趴在偏房一處隱蔽的房頂上張望吨拗。 院中可真熱鬧,春花似錦婿斥、人聲如沸劝篷。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)携龟。三九已至,卻和暖如春勘高,著一層夾襖步出監(jiān)牢的瞬間峡蟋,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工华望, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蕊蝗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓赖舟,卻偏偏與公主長(zhǎng)得像蓬戚,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宾抓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345