講到Java并發(fā)喧笔,多線程編程溃斋,一定避免不了對關(guān)鍵字volatile的了解吸申,那么如何來認識volatile,從哪些方面來了解它會比較合適呢截碴?
個人認為,既然是多線程編程走哺,那我們在平常的學習中哲虾,工作中,大部分都接觸到的就是線程安全的概念晒旅。
而線程安全就會涉及到共享變量的概念汪诉,所以首先,我們得弄清楚共享變量是什么拟烫,且處理器和內(nèi)存間的數(shù)據(jù)交互機制是如何導致共享變量變得不安全迄本。
共享變量
能夠在多個線程間被多個線程都訪問到的變量岸梨,我們稱之為共享變量。共享變量包括所有的實例變量曹阔,靜態(tài)變量和數(shù)組元素。他們都被存放在堆內(nèi)存中寂拆。
處理器與內(nèi)存的通信機制
大家都知道處理器是用來做計算的抓韩,且速度是非常快的尝江,而內(nèi)存是用來存儲數(shù)據(jù)的英上,且其訪問速度相比處理器來說,是慢了好幾個級別的惭聂。那么當處理器需要處理數(shù)據(jù)時相恃,如果每次都直接從內(nèi)存拿數(shù)據(jù)的話,就會導致效率非常低耕腾,因此在現(xiàn)代計算機系統(tǒng)中杀糯,處理器是不直接跟內(nèi)存通信的,而是在處理器和內(nèi)存之間設置了多個緩存火脉,也就是我們常常聽到的L1, L2, L3等高速緩存倦挂。
具體架構(gòu)如下所示:
處理器都是將數(shù)據(jù)從內(nèi)存讀到自己內(nèi)部的緩存中,然后在緩存中對數(shù)據(jù)進行修改等操作没炒,結(jié)束后再由緩存寫到回主存中去犯戏。
如果一個共享變量 X,在多線程的情況下种吸,同時被多個處理器讀到各自的緩存中去呀非,當其中一個處理器修改了X的值,改成Y了岸裙,先寫回了內(nèi)存降允,而此時另外一個處理器,又將X改成Z剧董,再寫回內(nèi)存,那么之前的Y就會被覆蓋掉了侠草。
這種情況下犁嗅,數(shù)據(jù)就已經(jīng)有問題了,這種因為多線程操作而導致的異常問題功蜓,通常我們就叫做線程不安全宠蚂。
如上述兩圖所示求厕,X的變量同時被不同的處理器修改成各自的Y和Z扰楼,那么如何避免這種情況呢弦赖?
這就涉及到了Java內(nèi)存模型中的可見性的概念浦辨。
Java內(nèi)存模型之可見性
可見性,意思就是說流酬,在多線程編程中芽腾,某個共享變量在其中一個線程被修改了,其修改結(jié)果要馬上能夠被其他線程看到晦嵌,拿上面的例子來說,也就是當X在其中一個處理器的緩存中被修改成Y了旱函, 另一個處理器必須能夠馬上知道自己緩存中的X已經(jīng)被修改成Y了描滔,當此處理器要拿此變量去參與計算的時候,必須重新去內(nèi)存中將此變量的值Y讀到緩存中券腔。
而一個變量拘泞,如果被聲明成violate,那么其就能保證這種可見性辱魁,這就是volatile變量的作用了诗鸭。
volatile
那么 volatile 變量能夠保證可見性的實現(xiàn)原理是什么?
聲明成volatile的變量强岸,在編譯成匯編指令的時候蝌箍,會多出以下一行:
0x0bca13ae:lock addl $0x0,(%esp) ;
這一句指令的意思是在寄存器上做一個+0的空操作暴心,但這條指令有個Lock前綴杂拨。
而處理器在處理Lock前綴指令時,其實是聲言了處理器的Lock#信號。
在之前的處理器中甚亭,Lock#信號會導致傳輸數(shù)據(jù)的總線被鎖定亏狰,其他處理器都不能訪問總線,從而保證處理Lock指令的處理器能夠獨享操作數(shù)據(jù)所在的內(nèi)存區(qū)域暇唾。
但由于總線被鎖住,其他的處理器都被堵住了瘸味,影響多處理器執(zhí)行的效率够挂。在后來的處理器中,聲言Lock#信號的處理器枯冈,不會再鎖住總線办悟,而是檢查到數(shù)據(jù)所在的內(nèi)存區(qū)域,如果是在處理器的內(nèi)部緩存中炫加,則會鎖定此緩存區(qū)域钠右,將緩存寫回到內(nèi)存當中看疗,并利用緩存一致性的原則來保證其他處理器中的緩存區(qū)域數(shù)據(jù)的一致性兼贸。
緩存一致性
緩存一致性原則會保證一個在緩存中的數(shù)據(jù)被修改了饥脑,會保證其他緩存了此數(shù)據(jù)的處理器中的緩存失效柬甥,從而讓處理器重新去內(nèi)存中讀取最新修改后的數(shù)據(jù)。
在實際的處理器操作中苛蒲,各個處理器會一直在總線上嗅探其內(nèi)部緩存區(qū)域中的內(nèi)存地址在其它處理器的操作情況臂外,一旦嗅探到某處理器打算修改某內(nèi)存地址,而此內(nèi)存地址剛好也在自己內(nèi)部的緩存中漏健,則會強制讓自己的緩存無效蔫浆。當下次訪問此內(nèi)存地址的時候,則重新從內(nèi)存當中讀取新數(shù)據(jù)瓦盛。
volatile不僅保證了共享變量在多線程間的可見性原环,其還保證了一定的有序性。
有序性
何謂有序性呢扮念?
事實上柜与,java程序代碼在編譯器階段和處理器執(zhí)行階段,為了優(yōu)化執(zhí)行的效率弄匕,有可能會對指令進行重排序。
如果一些指令彼此之間互相不影響剩瓶,那么就有可能不按照代碼順序執(zhí)行城丧,比如后面的代碼先執(zhí)行亡哄,而之前的代碼則慢執(zhí)行,但處理器會保證結(jié)束時的輸出結(jié)果是一致的。
以上的這種情況就說明指令有可能不是有序的灵临。
volatile變量趴荸,上面我們看過其匯編指令,會多出一條Lock前綴的指令顿涣,這條指令能夠 保證酝豪,在這條指令之前的所有指令全部執(zhí)行完畢,而在這條指令之后的所有指令全部未執(zhí)行,也相于在這里立起了一道柵欄锄码,稱之為內(nèi)存柵欄滋捶,而更通俗的說法,則是內(nèi)存屏障重窟。
那么有了這道屏障巡扇,volatile變量就禁止了指令的重排序,從而保證了指令執(zhí)行的有序性厅翔。
所有對volatile變量的讀操作一定發(fā)生在對volatile變量的寫操作之后刀闷。這同時也說明了volatile變量在多個線程之間能夠?qū)崿F(xiàn)可見性的原理。所以各種規(guī)定和操作顽分,其實之間互有關(guān)聯(lián)施蜜,彼此依賴,才能更好地保證指令執(zhí)行的準確和效率花墩。
內(nèi)存屏障
在上面我們也引出了內(nèi)存屏障的概念悬秉,也知道了澄步,其實它就是一組處理器的操作指令。
插入一個內(nèi)存屏障和泌,則相當于告訴處理器和編譯器先于這個指令的必須先執(zhí)行村缸,后于這個指令的必須后執(zhí)行。
內(nèi)存屏障另一個作用是強制更新一次不同CPU的緩存武氓。
例如梯皿,一個寫屏障會把這個屏障前寫入的數(shù)據(jù)刷新到緩存,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值县恕,而不用考慮到底是被哪個cpu核心或者哪顆CPU執(zhí)行的东羹。
這再仔細一想忠烛,不就是上面所說的volatile的作用嗎属提?
所以,內(nèi)存屏障美尸,可見性冤议,有序性,緩存一致性原則师坎,在java并發(fā)中各種各樣的名詞恕酸,本質(zhì)上可能就只是同一種現(xiàn)象或者同一種設計,從不同的角度觀察和探討所得出的不同的解釋胯陋。
下一篇文章
Java并發(fā)系列之synchronized