Volatile變量

????Java 語言提供了一種稍弱的同步機(jī)制,即volatile變量叶组,用來確保將變量的更新操作通知到其他線程拯田。當(dāng)把變量申明為volatile 類型后,編譯器與運(yùn)行時(shí)都會(huì)注意到這個(gè)變量是共享的扶叉,因此不會(huì)將該變量操作一起重排序勿锅。

一.內(nèi)存模型的相關(guān)概念

  大家都知道,計(jì)算機(jī)在執(zhí)行程序時(shí)枣氧,每條指令都是在CPU中執(zhí)行的溢十,而執(zhí)行指令過程中,勢必涉及到數(shù)據(jù)的讀取和寫入达吞。由于程序運(yùn)行過程中的臨時(shí)數(shù)據(jù)是存放在主存(物理內(nèi)存)當(dāng)中的张弛,這時(shí)就存在一個(gè)問題,由于CPU執(zhí)行速度很快,而從內(nèi)存讀取數(shù)據(jù)和向內(nèi)存寫入數(shù)據(jù)的過程跟CPU執(zhí)行指令的速度比起來要慢的多吞鸭,因此如果任何時(shí)候?qū)?shù)據(jù)的操作都要通過和內(nèi)存的交互來進(jìn)行寺董,會(huì)大大降低指令執(zhí)行的速度。因此在CPU里面就有了高速緩存刻剥。

  也就是遮咖,當(dāng)程序在運(yùn)行過程中,會(huì)將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當(dāng)中造虏,那么CPU進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù)御吞,當(dāng)運(yùn)算結(jié)束之后,再將高速緩存中的數(shù)據(jù)刷新到主存當(dāng)中漓藕。舉個(gè)簡單的例子陶珠,比如下面的這段代碼:

i = i +?1;

?  當(dāng)線程執(zhí)行這個(gè)語句時(shí),會(huì)先從主存當(dāng)中讀取i的值享钞,然后復(fù)制一份到高速緩存當(dāng)中揍诽,然后CPU執(zhí)行指令對i進(jìn)行加1操作,然后將數(shù)據(jù)寫入高速緩存栗竖,最后將高速緩存中i最新的值刷新到主存當(dāng)中暑脆。

  這個(gè)代碼在單線程中運(yùn)行是沒有任何問題的,但是在多線程中運(yùn)行就會(huì)有問題了划滋。在多核CPU中饵筑,每條線程可能運(yùn)行于不同的CPU中,因此每個(gè)線程運(yùn)行時(shí)有自己的高速緩存(對單核CPU來說处坪,其實(shí)也會(huì)出現(xiàn)這種問題根资,只不過是以線程調(diào)度的形式來分別執(zhí)行的)。本文我們以多核CPU為例同窘。

  比如同時(shí)有2個(gè)線程執(zhí)行這段代碼玄帕,假如初始時(shí)i的值為0,那么我們希望兩個(gè)線程執(zhí)行完之后i的值變?yōu)?想邦。但是事實(shí)會(huì)是這樣嗎裤纹?

  可能存在下面一種情況:初始時(shí),兩個(gè)線程分別讀取i的值存入各自所在的CPU的高速緩存當(dāng)中丧没,然后線程1進(jìn)行加1操作鹰椒,然后把i的最新值1寫入到內(nèi)存。此時(shí)線程2的高速緩存當(dāng)中i的值還是0呕童,進(jìn)行加1操作之后漆际,i的值為1,然后線程2把i的值寫入內(nèi)存夺饲。

  最終結(jié)果i的值是1奸汇,而不是2施符。這就是著名的緩存一致性問題。通常稱這種被多個(gè)線程訪問的變量為共享變量擂找。

  也就是說戳吝,如果一個(gè)變量在多個(gè)CPU中都存在緩存(一般在多線程編程時(shí)才會(huì)出現(xiàn)),那么就可能存在緩存不一致的問題贯涎。

  為了解決緩存不一致性問題听哭,通常來說有以下2種解決方法:

  1)通過在總線加LOCK#鎖的方式

  2)通過緩存一致性協(xié)議

  這2種方式都是硬件層面上提供的方式。

  在早期的CPU當(dāng)中柬采,是通過在總線上加LOCK#鎖的形式來解決緩存不一致的問題欢唾。因?yàn)镃PU和其他部件進(jìn)行通信都是通過總線來進(jìn)行的,如果對總線加LOCK#鎖的話粉捻,也就是說阻塞了其他CPU對其他部件訪問(如內(nèi)存),從而使得只能有一個(gè)CPU能使用這個(gè)變量的內(nèi)存斑芜。比如上面例子中 如果一個(gè)線程在執(zhí)行 i = i +1肩刃,如果在執(zhí)行這段代碼的過程中,在總線上發(fā)出了LCOK#鎖的信號(hào)杏头,那么只有等待這段代碼完全執(zhí)行完畢之后盈包,其他CPU才能從變量i所在的內(nèi)存讀取變量,然后進(jìn)行相應(yīng)的操作醇王。這樣就解決了緩存不一致的問題呢燥。

  但是上面的方式會(huì)有一個(gè)問題,由于在鎖住總線期間寓娩,其他CPU無法訪問內(nèi)存叛氨,導(dǎo)致效率低下。

  所以就出現(xiàn)了緩存一致性協(xié)議棘伴。最出名的就是Intel 的MESI協(xié)議寞埠,MESI協(xié)議保證了每個(gè)緩存中使用的共享變量的副本是一致的。它核心的思想是:當(dāng)CPU寫數(shù)據(jù)時(shí)焊夸,如果發(fā)現(xiàn)操作的變量是共享變量仁连,即在其他CPU中也存在該變量的副本,會(huì)發(fā)出信號(hào)通知其他CPU將該變量的緩存行置為無效狀態(tài)阱穗,因此當(dāng)其他CPU需要讀取這個(gè)變量時(shí)饭冬,發(fā)現(xiàn)自己緩存中緩存該變量的緩存行是無效的,那么它就會(huì)從內(nèi)存重新讀取揪阶。


圖1

二.并發(fā)編程中的三個(gè)概念

  在并發(fā)編程中昌抠,我們通常會(huì)遇到以下三個(gè)問題:原子性問題,可見性問題遣钳,有序性問題扰魂。我們先看具體看一下這三個(gè)概念:

1.原子性

  原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷忿晕,要么就都不執(zhí)行堰燎。

  一個(gè)很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題:

  比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個(gè)操作:從賬戶A減去1000元,往賬戶B加上1000元尼啡。

  試想一下,如果這2個(gè)操作不具備原子性蹦魔,會(huì)造成什么樣的后果啄栓。假如從賬戶A減去1000元之后,操作突然中止姻成。然后又從B取出了500元插龄,取出500元之后,再執(zhí)行 往賬戶B加上1000元 的操作科展。這樣就會(huì)導(dǎo)致賬戶A雖然減去了1000元均牢,但是賬戶B沒有收到這個(gè)轉(zhuǎn)過來的1000元。

  所以這2個(gè)操作必須要具備原子性才能保證不出現(xiàn)一些意外的問題才睹。

  同樣地反映到并發(fā)編程中會(huì)出現(xiàn)什么結(jié)果呢徘跪?

  舉個(gè)最簡單的例子,大家想一下假如為一個(gè)32位的變量賦值過程不具備原子性的話琅攘,會(huì)發(fā)生什么后果垮庐?

i =?9;

?  假若一個(gè)線程執(zhí)行到這個(gè)語句時(shí),我暫且假設(shè)為一個(gè)32位的變量賦值包括兩個(gè)過程:為低16位賦值坞琴,為高16位賦值哨查。

  那么就可能發(fā)生一種情況:當(dāng)將低16位數(shù)值寫入之后,突然被中斷剧辐,而此時(shí)又有一個(gè)線程去讀取i的值寒亥,那么讀取到的就是錯(cuò)誤的數(shù)據(jù)。

2.可見性

  可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí)浙于,一個(gè)線程修改了這個(gè)變量的值护盈,其他線程能夠立即看得到修改的值。

  舉個(gè)簡單的例子羞酗,看下面這段代碼:

//線程1執(zhí)行的代碼

int?i =?0;

i =?10;

//程2執(zhí)行的代碼

j = i;

?  假若執(zhí)行線程1的是CPU1腐宋,執(zhí)行線程2的是CPU2。由上面的分析可知檀轨,當(dāng)線程1執(zhí)行 i =10這句時(shí)胸竞,會(huì)先把i的初始值加載到CPU1的高速緩存中,然后賦值為10参萄,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了卫枝,卻沒有立即寫入到主存當(dāng)中。

  此時(shí)線程2執(zhí)行 j = i讹挎,它會(huì)先去主存讀取i的值并加載到CPU2的緩存當(dāng)中校赤,注意此時(shí)內(nèi)存當(dāng)中i的值還是0吆玖,那么就會(huì)使得j的值為0,而不是10.

  這就是可見性問題马篮,線程1對變量i修改了之后沾乘,線程2沒有立即看到線程1修改的值。

3.有序性

  有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行浑测。舉個(gè)簡單的例子翅阵,看下面這段代碼:

int?i =?0;??????????????

boolean?flag =?false;

i =?1;?//語句1??

flag =?true;?//語句2

?  上面代碼定義了一個(gè)int型變量,定義了一個(gè)boolean類型變量迁央,然后分別對兩個(gè)變量進(jìn)行賦值操作掷匠。從代碼順序上看,語句1是在語句2前面的岖圈,那么JVM在真正執(zhí)行這段代碼的時(shí)候會(huì)保證語句1一定會(huì)在語句2前面執(zhí)行嗎讹语?不一定,為什么呢蜂科?這里可能會(huì)發(fā)生指令重排序(Instruction Reorder)募强。

  下面解釋一下什么是指令重排序,一般來說崇摄,處理器為了提高程序運(yùn)行效率,可能會(huì)對輸入代碼進(jìn)行優(yōu)化慌烧,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致逐抑,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。

  比如上面的代碼中屹蚊,語句1和語句2誰先執(zhí)行對最終的程序結(jié)果并沒有影響厕氨,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行汹粤。

  但是要注意命斧,雖然處理器會(huì)對指令進(jìn)行重排序,但是它會(huì)保證程序最終結(jié)果會(huì)和代碼順序執(zhí)行結(jié)果相同嘱兼,那么它靠什么保證的呢国葬?再看下面一個(gè)例子:

int?a =?10;?//語句1

int?r =?2;?//語句2

a = a +?3;?//語句3

r = a*a;?//語句4

?  這段代碼有4個(gè)語句,那么可能的一個(gè)執(zhí)行順序是:

  那么可不可能是這個(gè)執(zhí)行順序呢: 語句2?? 語句1??? 語句4?? 語句3

  不可能芹壕,因?yàn)樘幚砥髟谶M(jìn)行重排序時(shí)是會(huì)考慮指令之間的數(shù)據(jù)依賴性汇四,如果一個(gè)指令I(lǐng)nstruction 2必須用到Instruction 1的結(jié)果,那么處理器會(huì)保證Instruction 1會(huì)在Instruction 2之前執(zhí)行踢涌。

  雖然重排序不會(huì)影響單個(gè)線程內(nèi)程序執(zhí)行的結(jié)果通孽,但是多線程呢?下面看一個(gè)例子:

context = loadContext();?//語句1

inited =?true;?//語句2

//線程2:

while(!inited ){

??sleep()

}

doSomethingwithconfig(context);

?  上面代碼中睁壁,由于語句1和語句2沒有數(shù)據(jù)依賴性背苦,因此可能會(huì)被重排序互捌。假如發(fā)生了重排序,在線程1執(zhí)行過程中先執(zhí)行語句2行剂,而此是線程2會(huì)以為初始化工作已經(jīng)完成秕噪,那么就會(huì)跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法硼讽,而此時(shí)context并沒有被初始化巢价,就會(huì)導(dǎo)致程序出錯(cuò)。

?  從上面可以看出固阁,指令重排序不會(huì)影響單個(gè)線程的執(zhí)行壤躲,但是會(huì)影響到線程并發(fā)執(zhí)行的正確性。

  也就是說备燃,要想并發(fā)程序正確地執(zhí)行碉克,必須要保證原子性、可見性以及有序性并齐。只要有一個(gè)沒有被保證漏麦,就有可能會(huì)導(dǎo)致程序運(yùn)行不正確。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末况褪,一起剝皮案震驚了整個(gè)濱河市撕贞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌测垛,老刑警劉巖捏膨,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異食侮,居然都是意外死亡号涯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門锯七,熙熙樓的掌柜王于貴愁眉苦臉地迎上來链快,“玉大人,你說我怎么就攤上這事眉尸∮蛭希” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵效五,是天一觀的道長地消。 經(jīng)常有香客問我,道長畏妖,這世上最難降的妖魔是什么脉执? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮戒劫,結(jié)果婚禮上半夷,老公的妹妹穿的比我還像新娘婆廊。我一直安慰自己,他們只是感情好巫橄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布淘邻。 她就那樣靜靜地躺著,像睡著了一般湘换。 火紅的嫁衣襯著肌膚如雪宾舅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天彩倚,我揣著相機(jī)與錄音筹我,去河邊找鬼。 笑死帆离,一個(gè)胖子當(dāng)著我的面吹牛蔬蕊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哥谷,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼岸夯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了们妥?” 一聲冷哼從身側(cè)響起猜扮,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎监婶,沒想到半個(gè)月后破镰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡压储,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了源譬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片集惋。...
    茶點(diǎn)故事閱讀 39,992評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖踩娘,靈堂內(nèi)的尸體忽然破棺而出刮刑,到底是詐尸還是另有隱情,我是刑警寧澤养渴,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布雷绢,位于F島的核電站,受9級特大地震影響理卑,放射性物質(zhì)發(fā)生泄漏翘紊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一藐唠、第九天 我趴在偏房一處隱蔽的房頂上張望帆疟。 院中可真熱鬧鹉究,春花似錦、人聲如沸踪宠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柳琢。三九已至绍妨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間柬脸,已是汗流浹背他去。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肖粮,地道東北人孤页。 一個(gè)月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像涩馆,于是被迫代替她去往敵國和親行施。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評論 2 355

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