對(duì)volatile不具有原子性的理解

在閱讀多線程書籍的時(shí)候鞍爱,對(duì)volatile的原子性產(chǎn)生了疑問举娩,問題類似于這篇文章所闡述的那樣。經(jīng)過一番思考給出自己的理解皿哨。
我們知道對(duì)于可見性,Java提供了volatile關(guān)鍵字來保證可見性纽谒、有序性往史。但不保證原子性
普通的共享變量不能保證可見性佛舱,因?yàn)槠胀ü蚕碜兞勘恍薷闹笞道裁磿r(shí)候被寫入主存是不確定的,當(dāng)其他線程去讀取時(shí)请祖,此時(shí)內(nèi)存中可能還是原來的舊值订歪,因此無法保證可見性。


??背景:為了提高處理速度肆捕,處理器不直接和內(nèi)存進(jìn)行通信刷晋,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作,但操作完不知道何時(shí)會(huì)寫到內(nèi)存眼虱。

  • 如果對(duì)聲明了volatile的變量進(jìn)行寫操作喻奥,JVM就會(huì)向處理器發(fā)送一條指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存捏悬。但是撞蚕,就算寫回到內(nèi)存,如果其他處理器緩存的值還是舊的过牙,再執(zhí)行計(jì)算操作就會(huì)有問題甥厦。
  • 在多處理器下,為了保證各個(gè)處理器的緩存是一致的寇钉,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議刀疙,每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改扫倡,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài)谦秧,當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里撵溃。

總結(jié)下來

  • 第一:使用volatile關(guān)鍵字會(huì)強(qiáng)制將修改的值立即寫入主存油够;
  • 第二:使用volatile關(guān)鍵字的話,當(dāng)線程2進(jìn)行修改時(shí)征懈,會(huì)導(dǎo)致線程1的工作內(nèi)存中緩存變量的緩存行無效(反映到硬件層的話石咬,就是CPU的L1或者L2緩存中對(duì)應(yīng)的緩存行無效);
  • 第三:由于線程1的工作內(nèi)存中緩存變量的緩存行無效卖哎,所以線程1再次讀取變量的值時(shí)會(huì)去主存讀取鬼悠。

最重要的是

  • 可見性:對(duì)一個(gè)volatile變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫入亏娜。
  • 原子性:對(duì)任意單個(gè)volatile變量的讀/寫具有原子性焕窝,但類似于volatile++這種復(fù)合操作不具有原子性。

舉2個(gè)例子维贺,例子來源于這篇文章:

例子是這樣的:

//線程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//線程2
stop = true;

原文:這段代碼是很典型的一段代碼它掂,很多人在中斷線程時(shí)可能都會(huì)采用這種標(biāo)記辦法。但是事實(shí)上溯泣,這段代碼會(huì)完全運(yùn)行正確么虐秋?即一定會(huì)將線程中斷么?不一定垃沦,也許在大多數(shù)時(shí)候客给,這個(gè)代碼能夠把線程中斷,但是也有可能會(huì)導(dǎo)致無法中斷線程(雖然這個(gè)可能性很小肢簿,但是只要一旦發(fā)生這種情況就會(huì)造成死循環(huán)了)靶剑。
  下面解釋一下這段代碼為何有可能導(dǎo)致無法中斷線程蜻拨。在前面已經(jīng)解釋過,每個(gè)線程在運(yùn)行過程中都有自己的工作內(nèi)存桩引,那么線程1在運(yùn)行的時(shí)候缎讼,會(huì)將stop變量的值拷貝一份放在自己的工作內(nèi)存當(dāng)中。
  那么當(dāng)線程2更改了stop變量的值之后坑匠,但是還沒來得及寫入主存當(dāng)中血崭,線程2轉(zhuǎn)去做其他事情了,那么線程1由于不知道線程2對(duì)stop變量的更改笛辟,因此還會(huì)一直循環(huán)下去功氨。
  但是用volatile修飾之后就變得不一樣了:
  第一:使用volatile關(guān)鍵字會(huì)強(qiáng)制將修改的值立即寫入主存序苏;
  第二:使用volatile關(guān)鍵字的話手幢,當(dāng)線程2進(jìn)行修改時(shí),會(huì)導(dǎo)致線程1的工作內(nèi)存中緩存變量stop的緩存行無效(反映到硬件層的話忱详,就是CPU的L1或者L2緩存中對(duì)應(yīng)的緩存行無效)围来;
  第三:由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時(shí)會(huì)去主存讀取匈睁。
到這里可能看起來沒什么問題,我們來看例子2

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }     
    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.inc);
    }
}

原文:大家想一下這段程序的輸出結(jié)果是多少监透?也許有些朋友認(rèn)為是10000。但是事實(shí)上運(yùn)行它會(huì)發(fā)現(xiàn)每次運(yùn)行結(jié)果都不一致航唆,都是一個(gè)小于10000的數(shù)字胀蛮。
  可能有的朋友就會(huì)有疑問,不對(duì)啊糯钙,上面是對(duì)變量inc進(jìn)行自增操作粪狼,由于volatile保證了可見性,那么在每個(gè)線程中對(duì)inc自增完之后任岸,在其他線程中都能看到修改后的值啊再榄,所以有10個(gè)線程分別進(jìn)行了1000次操作,那么最終inc的值應(yīng)該是1000*10=10000享潜。
  這里面就有一個(gè)誤區(qū)了困鸥,volatile關(guān)鍵字能保證可見性沒有錯(cuò),但是上面的程序錯(cuò)在沒能保證原子性剑按〖簿停可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對(duì)變量的操作的原子性艺蝴。
  在前面已經(jīng)提到過虐译,自增操作是不具備原子性的,它包括讀取變量的原始值吴趴、進(jìn)行加1操作漆诽、寫入工作內(nèi)存侮攀。那么就是說自增操作的三個(gè)子操作可能會(huì)分割開執(zhí)行,就有可能導(dǎo)致下面這種情況出現(xiàn):
  假如某個(gè)時(shí)刻變量inc的值為10厢拭,
  線程1對(duì)變量進(jìn)行自增操作兰英,線程1先讀取了變量inc的原始值,然后線程1被阻塞了供鸠;
  然后線程2對(duì)變量進(jìn)行自增操作畦贸,線程2也去讀取變量inc的原始值,由于線程1只是對(duì)變量inc進(jìn)行讀取操作楞捂,而沒有對(duì)變量進(jìn)行修改操作薄坏,所以不會(huì)導(dǎo)致線程2的工作內(nèi)存中緩存變量inc的緩存行無效,所以線程2會(huì)直接去主存讀取inc的值寨闹,發(fā)現(xiàn)inc的值時(shí)10胶坠,然后進(jìn)行加1操作,并把11寫入工作內(nèi)存繁堡,最后寫入主存沈善。
  然后線程1接著進(jìn)行加1操作,由于已經(jīng)讀取了inc的值椭蹄,注意此時(shí)在線程1的工作內(nèi)存中inc的值仍然為10闻牡,所以線程1對(duì)inc進(jìn)行加1操作后inc的值為11,然后將11寫入工作內(nèi)存绳矩,最后寫入主存罩润。
  那么兩個(gè)線程分別進(jìn)行了一次自增操作后,inc只增加了1翼馆。
  解釋到這里割以,可能有朋友會(huì)有疑問,不對(duì)啊写妥,前面不是保證一個(gè)變量在修改volatile變量時(shí)拳球,會(huì)讓緩存行無效嗎?然后其他線程去讀就會(huì)讀到新的值珍特,對(duì)祝峻,這個(gè)沒錯(cuò)。這個(gè)就是上面的happens-before規(guī)則中的volatile變量規(guī)則扎筒,但是要注意莱找,線程1對(duì)變量進(jìn)行讀取操作之后,被阻塞了的話嗜桌,并沒有對(duì)inc值進(jìn)行修改奥溺。然后雖然volatile能保證線程2對(duì)變量inc的值讀取是從內(nèi)存中讀取的,但是線程1沒有進(jìn)行修改骨宠,所以線程2根本就不會(huì)看到修改的值浮定。

大家是不是有這樣的疑問:“線程1在讀取inc為10后被阻塞了相满,沒有進(jìn)行修改所以不會(huì)去通知其他線程,此時(shí)線程2拿到的還是10桦卒,這點(diǎn)可以理解立美。但是后來線程2修改了inc變成11后寫回主內(nèi)存,這下是修改了方灾,線程1再次運(yùn)行時(shí)建蹄,難道不會(huì)去主存中獲取最新的值嗎?按照volatile的定義裕偿,如果volatile修飾的變量發(fā)生了變化洞慎,其他線程應(yīng)該去主存中拿變化后的值才對(duì)啊嘿棘?”
??是不是還有:例子1中線程1先將stop=flase讀取到了工作內(nèi)存中劲腿,然后去執(zhí)行循環(huán)操作,線程2將stop=true寫入到主存后蔫巩,為什么線程1的工作內(nèi)存中stop=false會(huì)變成無效的谆棱?

其實(shí)嚴(yán)格的說快压,對(duì)任意單個(gè)volatile變量的讀/寫具有原子性圆仔,但類似于volatile++這種復(fù)合操作不具有原子性。在《Java并發(fā)編程的藝術(shù)》中有這一段描述:“在多處理器下蔫劣,為了保證各個(gè)處理器的緩存是一致的坪郭,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議,每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了脉幢,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改歪沃,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候嫌松,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里沪曙。”我們需要注意的是萎羔,這里的修改操作液走,是指的一個(gè)操作

  • 例子1中贾陷,因?yàn)槭莣hile語句缘眶,線程會(huì)不斷讀取stop的值來判斷是否為false,每一次判斷都是一個(gè)操作髓废。這里是從緩存中讀取巷懈。單個(gè)讀取操作是具有原子性的,所以當(dāng)例子1中的線程2修改了stop時(shí)慌洪,由于volatile變量的可見性顶燕,線程1再讀取stop時(shí)是最新的值凑保,為true。
  • 而例子2中涌攻,為什么自增操作會(huì)出現(xiàn)那樣的結(jié)果呢愉适?可以知道自增操作是三個(gè)原子操作組合而成的復(fù)合操作。在一個(gè)操作中癣漆,讀取了inc變量后维咸,是不會(huì)再讀取的inc的,所以它的值還是之前讀的10惠爽,它的下一步是自增操作癌蓖。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市婚肆,隨后出現(xiàn)的幾起案子租副,更是在濱河造成了極大的恐慌,老刑警劉巖较性,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件用僧,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡赞咙,警方通過查閱死者的電腦和手機(jī)责循,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來攀操,“玉大人院仿,你說我怎么就攤上這事∷俸停” “怎么了歹垫?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)颠放。 經(jīng)常有香客問我排惨,道長(zhǎng),這世上最難降的妖魔是什么碰凶? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任暮芭,我火速辦了婚禮,結(jié)果婚禮上痒留,老公的妹妹穿的比我還像新娘谴麦。我一直安慰自己,他們只是感情好伸头,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布匾效。 她就那樣靜靜地躺著,像睡著了一般恤磷。 火紅的嫁衣襯著肌膚如雪面哼。 梳的紋絲不亂的頭發(fā)上野宜,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音魔策,去河邊找鬼匈子。 笑死,一個(gè)胖子當(dāng)著我的面吹牛闯袒,可吹牛的內(nèi)容都是我干的虎敦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼政敢,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼其徙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喷户,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤唾那,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后褪尝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闹获,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年河哑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了避诽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡灾馒,死狀恐怖茎用,靈堂內(nèi)的尸體忽然破棺而出遣总,到底是詐尸還是另有隱情睬罗,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布旭斥,位于F島的核電站容达,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏垂券。R本人自食惡果不足惜花盐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望菇爪。 院中可真熱鬧算芯,春花似錦、人聲如沸凳宙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽氏涩。三九已至届囚,卻和暖如春有梆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背意系。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國打工泥耀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛔添。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓痰催,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親迎瞧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子陨囊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

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

  • 參考:Java并發(fā)編程:volatile關(guān)鍵字解析 一.內(nèi)存模型的相關(guān)概念 二.并發(fā)編程中的三個(gè)概念 三.Java...
    誰在烽煙彼岸閱讀 724評(píng)論 1 1
  • 此文章出自:<a href="http://www.cnblogs.com/dolphin0520/p/39203...
    zlb閱讀 626評(píng)論 0 6
  • 一“你睡覺的時(shí)候會(huì)看到什么?”“睡覺時(shí)沒有問題夹攒。煩惱總是出現(xiàn)在清醒時(shí)蜘醋。”“比如咏尝?”“不存在的人出現(xiàn)在眼前压语,不真實(shí)的...
    馬克周閱讀 511評(píng)論 0 1
  • 前言 在MJRefresh源碼閱讀1——結(jié)構(gòu)梳理中我們已經(jīng)說了MJRefreshHeader是整個(gè)控件的核心類,它...
    Wang66閱讀 931評(píng)論 0 5