【轉(zhuǎn)載】Java并發(fā)編程系列02 | 并發(fā)編程三大核心問題

2.jpg

寫在前面

編寫并發(fā)程序是比較困難的,因?yàn)椴l(fā)程序極易出現(xiàn)Bug,這些Bug有都是比較詭異的肠槽,很多都是沒辦法追蹤,而且難以復(fù)現(xiàn)奢啥。
要快速準(zhǔn)確的發(fā)現(xiàn)并解決這些問題秸仙,首先就是要弄清并發(fā)編程的本質(zhì),并發(fā)編程要解決的是什么問題桩盲。
本文將帶你深入理解并發(fā)編程要解決的三大問題:原子性寂纪、可見性、有序性赌结。

補(bǔ)充知識(shí)

硬件的發(fā)展中弊攘,一直存在一個(gè)矛盾抢腐,CPU、內(nèi)存襟交、I/O設(shè)備的速度差異迈倍。
速度排序:CPU >> 內(nèi)存 >> I/O設(shè)備
為了平衡這三者的速度差異,做了如下優(yōu)化:
CPU 增加了緩存捣域,以均衡內(nèi)存與CPU的速度差異啼染;
操作系統(tǒng)增加了進(jìn)程、線程焕梅,以分時(shí)復(fù)用CPU迹鹅,進(jìn)而均衡I/O設(shè)備與CPU的速度差異;
編譯程序優(yōu)化指令執(zhí)行次序贞言,使得緩存能夠得到更加合理地利用斜棚。

可見性

可見性是什么?

一個(gè)線程對(duì)共享變量的修改该窗,另外一個(gè)線程能夠立刻看到弟蚀,我們稱為可見性。
為什么會(huì)有可見性問題酗失?

對(duì)于如今的多核處理器义钉,每顆CPU都有自己的緩存,而緩存僅僅對(duì)它所在的處理器可見规肴,CPU緩存與內(nèi)存的數(shù)據(jù)不容易保證一致捶闸。
為了避免處理器停頓下來等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲,處理器使用寫緩沖區(qū)來臨時(shí)保存向內(nèi)存寫入的數(shù)據(jù)拖刃。寫緩沖區(qū)合并對(duì)同一內(nèi)存地址的多次寫删壮,并以批處理的方式刷新,也就是說寫緩沖區(qū)不會(huì)即時(shí)將數(shù)據(jù)刷新到主內(nèi)存中兑牡。
緩存不能及時(shí)刷新導(dǎo)致了可見性問題醉锅。

可見性問題舉例

public class Test {
public int a = 0;

public void increase() {
        a++;
    }

public static void main(String[] args) {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000; j++)
                        test.increase();
                };
            }.start();
        }

while (Thread.activeCount() > 1) {
// 保證前面的線程都執(zhí)行完
            Thread.yield();
        }
        System.out.println(test.a);
    }
}

目的:10個(gè)線程將inc加到10000。
結(jié)果:每次運(yùn)行发绢,得到的結(jié)果都小于10000硬耍。
原因分析:


3.png

假設(shè)線程1和線程2同時(shí)開始執(zhí)行,那么第一次都會(huì)將a=0 讀到各自的CPU緩存里边酒,線程1執(zhí)行a++之后a=1经柴,但是此時(shí)線程2是看不到線程1中a的值的,所以線程2里a=0墩朦,執(zhí)行a++后a=1坯认。
線程1和線程2各自CPU緩存里的值都是1,之后線程1和線程2都會(huì)將自己緩存中的a=1寫入內(nèi)存,導(dǎo)致內(nèi)存中a=1牛哺,而不是我們期望的2陋气。所以導(dǎo)致最終 a 的值都是小于 10000 的。這就是緩存的可見性問題引润。
原子性

原子性是什么巩趁?

把一個(gè)或者多個(gè)操作在 CPU 執(zhí)行的過程中不被中斷的特性稱為原子性。
在并發(fā)編程中淳附,原子性的定義不應(yīng)該和事務(wù)中的原子性(一旦代碼運(yùn)行異骋槲浚可以回滾)一樣。應(yīng)該理解為:一段代碼奴曙,或者一個(gè)變量的操作别凹,在一個(gè)線程沒有執(zhí)行完之前,不能被其他線程執(zhí)行洽糟。

為什么會(huì)有原子性問題炉菲?

線程是CPU調(diào)度的基本單位。CPU會(huì)根據(jù)不同的調(diào)度算法進(jìn)行線程調(diào)度坤溃,將時(shí)間片分派給線程拍霜。當(dāng)一個(gè)線程獲得時(shí)間片之后開始執(zhí)行,在時(shí)間片耗盡之后浇雹,就會(huì)失去CPU使用權(quán)。多線程場(chǎng)景下屿讽,由于時(shí)間片在線程間輪換昭灵,就會(huì)發(fā)生原子性問題。
如:對(duì)于一段代碼伐谈,一個(gè)線程還沒執(zhí)行完這段代碼但是時(shí)間片耗盡烂完,在等待CPU分配時(shí)間片,此時(shí)其他線程可以獲取執(zhí)行這段代碼的時(shí)間片來執(zhí)行這段代碼诵棵,導(dǎo)致多個(gè)線程同時(shí)執(zhí)行同一段代碼抠蚣,也就是原子性問題。

線程切換帶來原子性問題履澳。

在Java中嘶窄,對(duì)基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的距贷,要么執(zhí)行柄冲,要么不執(zhí)行

i = 0;      // 原子性操作
j = i;      // 不是原子性操作,包含了兩個(gè)操作:讀取i忠蝗,將i值賦值給j
i++;            // 不是原子性操作现横,包含了三個(gè)操作:讀取i值、i + 1 、將+1結(jié)果賦值給i
i = j + 1;      // 不是原子性操作戒祠,包含了三個(gè)操作:讀取j值骇两、j + 1 、將+1結(jié)果賦值給i

原子性問題舉例

還是上文中的代碼姜盈,10個(gè)線程將inc加到10000低千。假設(shè)在保證可見性的情況下,仍然會(huì)因?yàn)樵有詥栴}導(dǎo)致執(zhí)行結(jié)果達(dá)不到預(yù)期贩据。為方便看栋操,把代碼貼到這里:

public class Test {
public int a = 0;

public void increase() {
        a++;
    }

public static void main(String[] args) {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000; j++)
                        test.increase();
                };
            }.start();
        }

while (Thread.activeCount() > 1) {
// 保證前面的線程都執(zhí)行完
            Thread.yield();
        }
        System.out.println(test.a);
    }
}

目的:10個(gè)線程將inc加到10000。
結(jié)果:每次運(yùn)行饱亮,得到的結(jié)果都小于10000矾芙。
原因分析:

首先來看a++操作,其實(shí)包括三個(gè)操作: 
①讀取a=0; 
②計(jì)算0+1=1; 
③將1賦值給a; 
保證a++的原子性近上,就是保證這三個(gè)操作在一個(gè)線程沒有執(zhí)行完之前剔宪,不能被其他線程執(zhí)行。

實(shí)際執(zhí)行時(shí)序圖如下:


3.jpg

關(guān)鍵一步:線程2在讀取a的值時(shí)壹无,線程1還沒有完成a=1的賦值操作葱绒,導(dǎo)致線程2的計(jì)算結(jié)果也是a=1。
問題在于沒有保證a++操作的原子性斗锭。如果保證a++的原子性地淀,線程1在執(zhí)行完三個(gè)操作之前,線程2不能執(zhí)行a++岖是,那么就可以保證在線程2執(zhí)行a++時(shí)帮毁,讀取到a=1,從而得到正確的結(jié)果豺撑。

有序性

有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行烈疚。
編譯器為了優(yōu)化性能,有時(shí)候會(huì)改變程序中語句的先后順序聪轿。例如程序中:“a=6爷肝;b=7;”編譯器優(yōu)化后可能變成“b=7陆错;a=6灯抛;”,在這個(gè)例子中音瓷,編譯器調(diào)整了語句的順序牧愁,但是不影響程序的最終結(jié)果。不過有時(shí)候編譯器及解釋器的優(yōu)化可能導(dǎo)致意想不到的Bug外莲。
有序性問題舉例

Java中的一個(gè)經(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;
  }
}

在獲取實(shí)例getInstance()的方法中猪半,我們首先判斷 instance是否為空兔朦,如果為空,則鎖定 Singleton.class并再次檢查instance是否為空磨确,如果還為空則創(chuàng)建Singleton的一個(gè)實(shí)例沽甥。
看似很完美,既保證了線程完全的初始化單例乏奥,又經(jīng)過判斷instance為null時(shí)再用synchronized同步加鎖摆舟。但是還有問題!
instance = new Singleton(); 創(chuàng)建對(duì)象的代碼邓了,分為三步:
①分配內(nèi)存空間
②初始化對(duì)象Singleton
③將內(nèi)存空間的地址賦值給instance
但是這三步經(jīng)過重排之后:
①分配內(nèi)存空間
②將內(nèi)存空間的地址賦值給instance
③初始化對(duì)象Singleton
會(huì)導(dǎo)致什么結(jié)果呢恨诱?
線程A先執(zhí)行g(shù)etInstance()方法,當(dāng)執(zhí)行完指令②時(shí)恰好發(fā)生了線程切換骗炉,切換到了線程B上照宝;如果此時(shí)線程B也執(zhí)行g(shù)etInstance()方法,那么線程B在執(zhí)行第一個(gè)判斷時(shí)會(huì)發(fā)現(xiàn)instance!=null句葵,所以直接返回instance厕鹃,而此時(shí)的instance是沒有初始化過的,如果我們這個(gè)時(shí)候訪問instance的成員變量就可能觸發(fā)空指針異常乍丈。
執(zhí)行時(shí)序圖:

4.jpg

總結(jié)

并發(fā)編程的本質(zhì)就是解決三大問題:原子性剂碴、可見性、有序性轻专。
原子性:一個(gè)或者多個(gè)操作在 CPU 執(zhí)行的過程中不被中斷的特性忆矛。由于線程的切換,導(dǎo)致多個(gè)線程同時(shí)執(zhí)行同一段代碼请垛,帶來的原子性問題催训。
可見性:一個(gè)線程對(duì)共享變量的修改,另外一個(gè)線程能夠立刻看到叼屠。緩存不能及時(shí)刷新導(dǎo)致了可見性問題瞳腌。
有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行绞铃。編譯器為了優(yōu)化性能而改變程序中語句的先后順序镜雨,導(dǎo)致有序性問題。
啟發(fā):線程的切換儿捧、緩存及編譯優(yōu)化都是為了提高性能荚坞,但是引發(fā)了并發(fā)編程的問題。這也告訴我們技術(shù)在解決一個(gè)問題時(shí)菲盾,必然會(huì)帶來另一個(gè)問題颓影,需要我們提前考慮新技術(shù)帶來的問題以規(guī)避風(fēng)險(xiǎn)。

轉(zhuǎn)載自 公眾號(hào) <java進(jìn)階架構(gòu)師>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末懒鉴,一起剝皮案震驚了整個(gè)濱河市诡挂,隨后出現(xiàn)的幾起案子碎浇,更是在濱河造成了極大的恐慌,老刑警劉巖璃俗,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奴璃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡城豁,警方通過查閱死者的電腦和手機(jī)苟穆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唱星,“玉大人雳旅,你說我怎么就攤上這事〖淞模” “怎么了攒盈?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)甸饱。 經(jīng)常有香客問我沦童,道長(zhǎng),這世上最難降的妖魔是什么叹话? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任偷遗,我火速辦了婚禮,結(jié)果婚禮上驼壶,老公的妹妹穿的比我還像新娘氏豌。我一直安慰自己,他們只是感情好热凹,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布泵喘。 她就那樣靜靜地躺著,像睡著了一般般妙。 火紅的嫁衣襯著肌膚如雪纪铺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天碟渺,我揣著相機(jī)與錄音鲜锚,去河邊找鬼。 笑死苫拍,一個(gè)胖子當(dāng)著我的面吹牛芜繁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播绒极,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼骏令,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了垄提?” 一聲冷哼從身側(cè)響起榔袋,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤周拐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后凰兑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體速妖,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年聪黎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了罕容。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡稿饰,死狀恐怖锦秒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情喉镰,我是刑警寧澤旅择,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站侣姆,受9級(jí)特大地震影響生真,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捺宗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一柱蟀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚜厉,春花似錦长已、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贰健,卻和暖如春胞四,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伶椿。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來泰國打工辜伟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悬垃。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓游昼,卻偏偏與公主長(zhǎng)得像甘苍,于是被迫代替她去往敵國和親尝蠕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

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