【漫畫】JAVA并發(fā)編程三大Bug源頭(可見性户辞、原子性、有序性)

原創(chuàng)聲明:本文轉(zhuǎn)載自公眾號(hào)【胖滾豬學(xué)編程】?

某日癞谒,胖滾豬寫的代碼導(dǎo)致了一個(gè)生產(chǎn)bug底燎,奮戰(zhàn)到凌晨三點(diǎn)依舊沒有解決問題。胖滾熊一看弹砚,只用了一個(gè)volatile就解決了双仍。并告知胖滾豬,這是并發(fā)編程導(dǎo)致的坑桌吃。這讓胖滾豬堅(jiān)定了要學(xué)好并發(fā)編程的決心朱沃。。于是茅诱,開始了我們并發(fā)編程的第一課为流。

序幕

con2

BUG源頭之一:可見性

剛剛我們說到,CPU緩存可以提高程序性能让簿,但緩存也是造成BUG源頭之一敬察,因?yàn)榫彺婵梢詫?dǎo)致可見性問題。我們先來看一段代碼:

private static int count = 0;
public static void main(String[] args) throws Exception {
    Thread th1 = new Thread(() -> {
        count = 10;
    });
    Thread th2 = new Thread(() -> {
        //極小概率會(huì)出現(xiàn)等于0的情況
        System.out.println("count=" + count);
    });
    th1.start();
    th2.start();
}

按理來說尔当,應(yīng)該正確返回10莲祸,但結(jié)果卻有可能是0蹂安。

一個(gè)線程對(duì)變量的改變另一個(gè)線程沒有g(shù)et到,這就是可見性導(dǎo)致的bug锐帜。一個(gè)線程對(duì)共享變量的修改田盈,另外一個(gè)線程能夠立刻看到,我們稱為可見性缴阎。

那么在談?wù)摽梢娦詥栴}之前允瞧,你必須了解下JAVA的內(nèi)存模型,我繪制了一張圖來描述:

JAVA_

主內(nèi)存(Main Memory)

主內(nèi)存可以簡(jiǎn)單理解為計(jì)算機(jī)當(dāng)中的內(nèi)存蛮拔,但又不完全等同述暂。主內(nèi)存被所有的線程所共享,對(duì)于一個(gè)共享變量(比如靜態(tài)變量建炫,或是堆內(nèi)存中的實(shí)例)來說畦韭,主內(nèi)存當(dāng)中存儲(chǔ)了它的“本尊”。

工作內(nèi)存(Working Memory)

工作內(nèi)存可以簡(jiǎn)單理解為計(jì)算機(jī)當(dāng)中的CPU高速緩存肛跌,但準(zhǔn)確的說它是涵蓋了緩存艺配、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化衍慎。每一個(gè)線程擁有自己的工作內(nèi)存转唉,對(duì)于一個(gè)共享變量來說,工作內(nèi)存當(dāng)中存儲(chǔ)了它的“副本”稳捆。

線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行赠法,而不能直接讀寫主內(nèi)存中的變量。
線程之間無法直接訪問對(duì)方的工作內(nèi)存中的變量眷柔,線程間變量的傳遞均需要通過主內(nèi)存來完成

現(xiàn)在再回到剛剛的問題期虾,為什么那段代碼會(huì)導(dǎo)致可見性問題呢原朝,根據(jù)內(nèi)存模型來分析驯嘱,我相信你會(huì)有答案了。當(dāng)多個(gè)線程在不同的 CPU 上執(zhí)行時(shí)喳坠,這些線程操作的是不同的 CPU 緩存鞠评。比如下圖中,線程 A 操作的是 CPU-1 上的緩存壕鹉,而線程 B 操作的是 CPU-2 上的緩存


image

由于線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行剃幌,而不能直接讀寫主內(nèi)存中的變量,那么對(duì)于共享變量V晾浴,它們首先是在自己的工作內(nèi)存负乡,之后再同步到主內(nèi)存〖够耍可是并不會(huì)及時(shí)的刷到主存中抖棘,而是會(huì)有一定時(shí)間差。很明顯,這個(gè)時(shí)候線程 A 對(duì)變量 V 的操作對(duì)于線程 B 而言就不具備可見性了 切省。

con3_1
private volatile long count = 0;
?
private void add10K() {
    int idx = 0;
    while (idx++ < 10000) {
        count++;
    }
}
?
public static void main(String[] args) throws InterruptedException {
    TestVolatile2 test = new TestVolatile2();
    // 創(chuàng)建兩個(gè)線程最岗,執(zhí)行 add() 操作
    Thread th1 = new Thread(()->{
        test.add10K();
    });
    Thread th2 = new Thread(()->{
        test.add10K();
    });
    // 啟動(dòng)兩個(gè)線程
    th1.start();
    th2.start();
    // 等待兩個(gè)線程執(zhí)行結(jié)束
    th1.join();
    th2.join();
    // 介于1w-2w,即使加了volatile也達(dá)不到2w
    System.out.println(test.count);
}
?
con3_2

原創(chuàng)聲明:本文轉(zhuǎn)載自公眾號(hào)【胖滾豬學(xué)編程】?

原子性問題

一個(gè)不可分割的操作叫做原子性操作,它不會(huì)被線程調(diào)度機(jī)制打斷的朝捆,這種操作一旦開始般渡,就一直運(yùn)行到結(jié)束,中間不會(huì)有任何線程切換芙盘。注意線程切換是重點(diǎn)驯用!

我們都知道CPU資源的分配都是以線程為單位的,并且是分時(shí)調(diào)用,操作系統(tǒng)允許某個(gè)進(jìn)程執(zhí)行一小段時(shí)間,例如 50 毫秒何陆,過了 50 毫秒操作系統(tǒng)就會(huì)重新選擇一個(gè)進(jìn)程來執(zhí)行(我們稱為“任務(wù)切換”)晨汹,這個(gè) 50 毫秒稱為“時(shí)間片”。而任務(wù)的切換大多數(shù)是在時(shí)間片段結(jié)束以后,

_

那么線程切換為什么會(huì)帶來bug呢贷盲?因?yàn)椴僮飨到y(tǒng)做任務(wù)切換淘这,可以發(fā)生在任何一條CPU 指令執(zhí)行完!注意巩剖,是 CPU 指令铝穷,CPU 指令,CPU 指令佳魔,而不是高級(jí)語(yǔ)言里的一條語(yǔ)句曙聂。比如count++,在java里就是一句話鞠鲜,但高級(jí)語(yǔ)言里一條語(yǔ)句往往需要多條 CPU 指令完成宁脊。其實(shí)count++包含了三個(gè)CPU指令!

  • 指令 1:首先贤姆,需要把變量 count 從內(nèi)存加載到 CPU 的寄存器榆苞;
  • 指令 2:之后,在寄存器中執(zhí)行 +1 操作霞捡;
  • 指令 3:最后坐漏,將結(jié)果寫入內(nèi)存(緩存機(jī)制導(dǎo)致可能寫入的是 CPU 緩存而不是內(nèi)存)。

小技巧:可以寫一個(gè)簡(jiǎn)單的count++程序碧信,依次執(zhí)行javac TestCount.java赊琳,javap -c -s TestCount.class得到匯編指令,驗(yàn)證下count++確實(shí)是分成了多條指令的砰碴。

volatile雖然能保證執(zhí)行完及時(shí)把變量刷到主內(nèi)存中躏筏,但對(duì)于count++這種非原子性、多指令的情況呈枉,由于線程切換趁尼,線程A剛把count=0加載到工作內(nèi)存檐什,線程B就可以開始工作了,這樣就會(huì)導(dǎo)致線程A和B執(zhí)行完的結(jié)果都是1弱卡,都寫到主內(nèi)存中乃正,主內(nèi)存的值還是1不是2,下面這張圖形象表示了該歷程:

_
image

原創(chuàng)聲明:本文轉(zhuǎn)載自公眾號(hào)【胖滾豬學(xué)編程】?

有序性問題

JAVA為了優(yōu)化性能婶博,允許編譯器和處理器對(duì)指令進(jìn)行重排序瓮具,即有時(shí)候會(huì)改變程序中語(yǔ)句的先后順序:

例如程序中:“a=6;b=7凡人;”編譯器優(yōu)化后可能變成“b=7名党;a=6;”只是在這個(gè)程序中不影響程序的最終結(jié)果挠轴。

有序性指的是程序按照代碼的先后順序執(zhí)行传睹。但是不要望文生義,這里的順序不是按照代碼位置的依次順序執(zhí)行指令,指的是最終結(jié)果在我們看起來就像是有序的岸晦。

重排序的過程不會(huì)影響單線程程序的執(zhí)行欧啤,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性。有時(shí)候編譯器及解釋器的優(yōu)化可能導(dǎo)致意想不到的 Bug启上。比如非常經(jīng)典的雙重檢查創(chuàng)建單例對(duì)象邢隧。

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

你可能會(huì)覺得這個(gè)程序天衣無縫,我兩次判斷是否為空冈在,還用了synchronized倒慧,剛剛也說了,synchronized 是獨(dú)占鎖/排他鎖包券。按照常理來說纫谅,應(yīng)該是這么一個(gè)邏輯:
線程A和B同時(shí)進(jìn)來,判斷instance == null溅固,線程A先獲取了鎖付秕,B等待,然后線程 A 會(huì)創(chuàng)建一個(gè) Singleton 實(shí)例发魄,之后釋放鎖盹牧,鎖釋放后俩垃,線程 B 被喚醒励幼,線程 B 再次嘗試加鎖,此時(shí)加鎖會(huì)成功口柳,然后線程 B 檢查 instance == null 時(shí)會(huì)發(fā)現(xiàn)苹粟,已經(jīng)創(chuàng)建過 Singleton 實(shí)例了,所以線程 B 不會(huì)再創(chuàng)建一個(gè) Singleton 實(shí)例跃闹。

但多線程往往要有非常理性的思維嵌削,我們先分析一下 instance = new Singleton()這句話毛好,根據(jù)剛剛原子性說到的,一句高級(jí)語(yǔ)言在cpu層面其實(shí)是多條指令苛秕,這也不例外肌访,我們也很熟悉new了,它會(huì)分為以下幾條指令:
1艇劫、分配一塊內(nèi)存 M吼驶;
2、在內(nèi)存 M 上初始化 Singleton 對(duì)象店煞;
3蟹演、然后 M 的地址賦值給 instance 變量。

如果真按照上述三條指令執(zhí)行是沒問題的顷蟀,但經(jīng)過編譯優(yōu)化后的執(zhí)行路徑卻是這樣的:
1酒请、分配一塊內(nèi)存 M;
2鸣个、將 M 的地址賦值給 instance 變量羞反;
3、最后在內(nèi)存 M 上初始化 Singleton 對(duì)象

假如當(dāng)執(zhí)行完指令 2 時(shí)恰好發(fā)生了線程切換囤萤,切換到了線程 B 上苟弛;而此時(shí)線程 B 也執(zhí)行 getInstance() 方法,那么線程 B 在執(zhí)行第一個(gè)判斷時(shí)會(huì)發(fā)現(xiàn) instance != null 阁将,所以直接返回 instance膏秫,而此時(shí)的 instance 是沒有初始化過的,如果我們這個(gè)時(shí)候訪問 instance 的成員變量就可能觸發(fā)空指針異常做盅,如圖所示:

_
con4

總結(jié)

并發(fā)程序是一把雙刃劍缤削,一方面大幅度提升了程序性能,另一方面帶來了很多隱藏的無形的難以發(fā)現(xiàn)的bug吹榴。我們首先要知道并發(fā)程序的問題在哪里亭敢,只有確定了“靶子”,才有可能把問題解決图筹,畢竟所有的解決方案都是針對(duì)問題的帅刀。并發(fā)程序經(jīng)常出現(xiàn)的詭異問題看上去非常無厘頭,但是只要我們能夠深刻理解可見性远剩、原子性扣溺、有序性在并發(fā)場(chǎng)景下的原理,很多并發(fā) Bug 都是可以理解瓜晤、可以診斷的锥余。
總結(jié)一句話:可見性是緩存導(dǎo)致的,而線程切換會(huì)帶來的原子性問題痢掠,編譯優(yōu)化會(huì)帶來有序性問題驱犹。至于怎么解決呢嘲恍!欲知后事如何,且聽下回分解雄驹。

原創(chuàng)聲明:本文轉(zhuǎn)載自公眾號(hào)【胖滾豬學(xué)編程】?

本文轉(zhuǎn)載自公眾號(hào)【胖滾豬學(xué)編程】 用漫畫讓編程so easy and interesting佃牛!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市医舆,隨后出現(xiàn)的幾起案子吁脱,更是在濱河造成了極大的恐慌,老刑警劉巖彬向,帶你破解...
    沈念sama閱讀 223,126評(píng)論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兼贡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡娃胆,警方通過查閱死者的電腦和手機(jī)遍希,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來里烦,“玉大人凿蒜,你說我怎么就攤上這事⌒埠冢” “怎么了废封?”我有些...
    開封第一講書人閱讀 169,941評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)丧蘸。 經(jīng)常有香客問我漂洋,道長(zhǎng),這世上最難降的妖魔是什么力喷? 我笑而不...
    開封第一講書人閱讀 60,294評(píng)論 1 300
  • 正文 為了忘掉前任刽漂,我火速辦了婚禮,結(jié)果婚禮上弟孟,老公的妹妹穿的比我還像新娘贝咙。我一直安慰自己,他們只是感情好拂募,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評(píng)論 6 398
  • 文/花漫 我一把揭開白布庭猩。 她就那樣靜靜地躺著,像睡著了一般陈症。 火紅的嫁衣襯著肌膚如雪蔼水。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,874評(píng)論 1 314
  • 那天爬凑,我揣著相機(jī)與錄音徙缴,去河邊找鬼试伙。 笑死嘁信,一個(gè)胖子當(dāng)著我的面吹牛于样,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播潘靖,決...
    沈念sama閱讀 41,285評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼穿剖,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了卦溢?” 一聲冷哼從身側(cè)響起糊余,我...
    開封第一講書人閱讀 40,249評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎单寂,沒想到半個(gè)月后贬芥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,760評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宣决,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評(píng)論 3 343
  • 正文 我和宋清朗相戀三年蘸劈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尊沸。...
    茶點(diǎn)故事閱讀 40,973評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡威沫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出洼专,到底是詐尸還是另有隱情棒掠,我是刑警寧澤,帶...
    沈念sama閱讀 36,631評(píng)論 5 351
  • 正文 年R本政府宣布屁商,位于F島的核電站烟很,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蜡镶。R本人自食惡果不足惜溯职,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望帽哑。 院中可真熱鬧谜酒,春花似錦、人聲如沸妻枕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)屡谐。三九已至述么,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間愕掏,已是汗流浹背度秘。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評(píng)論 1 275
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人剑梳。 一個(gè)月前我還...
    沈念sama閱讀 49,431評(píng)論 3 379
  • 正文 我出身青樓唆貌,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親垢乙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子锨咙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評(píng)論 2 361