講到Java并發(fā),多線程編程,一定避免不了對(duì)關(guān)鍵字volatile的了解,那么如何來(lái)認(rèn)識(shí)volatile,從哪些方面來(lái)了解它會(huì)比較合適呢?
個(gè)人認(rèn)為,既然是多線程編程,那我們?cè)谄匠5膶W(xué)習(xí)中,工作中,大部分都接觸到的就是線程安全的概念。
而線程安全就會(huì)涉及到共享變量的概念,所以首先,我們得弄清楚共享變量是什么,且處理器和內(nèi)存間的數(shù)據(jù)交互機(jī)制是如何導(dǎo)致共享變量變得不安全。
共享變量
能夠在多個(gè)線程間被多個(gè)線程都訪問(wèn)到的變量,我們稱之為共享變量。共享變量包括所有的實(shí)例變量,靜態(tài)變量和數(shù)組元素。他們都被存放在堆內(nèi)存中。
處理器與內(nèi)存的通信機(jī)制
大家都知道處理器是用來(lái)做計(jì)算的,且速度是非??斓模鴥?nèi)存是用來(lái)存儲(chǔ)數(shù)據(jù)的,且其訪問(wèn)速度相比處理器來(lái)說(shuō),是慢了好幾個(gè)級(jí)別的。那么當(dāng)處理器需要處理數(shù)據(jù)時(shí),如果每次都直接從內(nèi)存拿數(shù)據(jù)的話,就會(huì)導(dǎo)致效率非常低,因此在現(xiàn)代計(jì)算機(jī)系統(tǒng)中,處理器是不直接跟內(nèi)存通信的,而是在處理器和內(nèi)存之間設(shè)置了多個(gè)緩存,也就是我們常常聽到的L1, L2, L3等高速緩存。
具體架構(gòu)如下所示:

處理器都是將數(shù)據(jù)從內(nèi)存讀到自己內(nèi)部的緩存中,然后在緩存中對(duì)數(shù)據(jù)進(jìn)行修改等操作,結(jié)束后再由緩存寫到回主存中去。
如果一個(gè)共享變量 X,在多線程的情況下,同時(shí)被多個(gè)處理器讀到各自的緩存中去,當(dāng)其中一個(gè)處理器修改了X的值,改成Y了,先寫回了內(nèi)存,而此時(shí)另外一個(gè)處理器,又將X改成Z,再寫回內(nèi)存,那么之前的Y就會(huì)被覆蓋掉了。
這種情況下,數(shù)據(jù)就已經(jīng)有問(wèn)題了,這種因?yàn)槎嗑€程操作而導(dǎo)致的異常問(wèn)題,通常我們就叫做線程不安全。


如上述兩圖所示,X的變量同時(shí)被不同的處理器修改成各自的Y和Z,那么如何避免這種情況呢?
這就涉及到了Java內(nèi)存模型中的可見性的概念。
Java內(nèi)存模型之可見性
可見性,意思就是說(shuō),在多線程編程中,某個(gè)共享變量在其中一個(gè)線程被修改了,其修改結(jié)果要馬上能夠被其他線程看到,拿上面的例子來(lái)說(shuō),也就是當(dāng)X在其中一個(gè)處理器的緩存中被修改成Y了, 另一個(gè)處理器必須能夠馬上知道自己緩存中的X已經(jīng)被修改成Y了,當(dāng)此處理器要拿此變量去參與計(jì)算的時(shí)候,必須重新去內(nèi)存中將此變量的值Y讀到緩存中。
而一個(gè)變量,如果被聲明成violate,那么其就能保證這種可見性,這就是volatile變量的作用了。
volatile
那么 volatile 變量能夠保證可見性的實(shí)現(xiàn)原理是什么?
聲明成volatile的變量,在編譯成匯編指令的時(shí)候,會(huì)多出以下一行:
0x0bca13ae:lock addl $0x0,(%esp) ;
這一句指令的意思是在寄存器上做一個(gè)+0的空操作,但這條指令有個(gè)Lock前綴。
而處理器在處理Lock前綴指令時(shí),其實(shí)是聲言了處理器的Lock#信號(hào)。
在之前的處理器中,Lock#信號(hào)會(huì)導(dǎo)致傳輸數(shù)據(jù)的總線被鎖定,其他處理器都不能訪問(wèn)總線,從而保證處理Lock指令的處理器能夠獨(dú)享操作數(shù)據(jù)所在的內(nèi)存區(qū)域。
但由于總線被鎖住,其他的處理器都被堵住了,影響多處理器執(zhí)行的效率。在后來(lái)的處理器中,聲言Lock#信號(hào)的處理器,不會(huì)再鎖住總線,而是檢查到數(shù)據(jù)所在的內(nèi)存區(qū)域,如果是在處理器的內(nèi)部緩存中,則會(huì)鎖定此緩存區(qū)域,將緩存寫回到內(nèi)存當(dāng)中,并利用緩存一致性的原則來(lái)保證其他處理器中的緩存區(qū)域數(shù)據(jù)的一致性。
緩存一致性
緩存一致性原則會(huì)保證一個(gè)在緩存中的數(shù)據(jù)被修改了,會(huì)保證其他緩存了此數(shù)據(jù)的處理器中的緩存失效,從而讓處理器重新去內(nèi)存中讀取最新修改后的數(shù)據(jù)。
在實(shí)際的處理器操作中,各個(gè)處理器會(huì)一直在總線上嗅探其內(nèi)部緩存區(qū)域中的內(nèi)存地址在其它處理器的操作情況,一旦嗅探到某處理器打算修改某內(nèi)存地址,而此內(nèi)存地址剛好也在自己內(nèi)部的緩存中,則會(huì)強(qiáng)制讓自己的緩存無(wú)效。當(dāng)下次訪問(wèn)此內(nèi)存地址的時(shí)候,則重新從內(nèi)存當(dāng)中讀取新數(shù)據(jù)。
volatile不僅保證了共享變量在多線程間的可見性,其還保證了一定的有序性。
有序性
何謂有序性呢?
事實(shí)上,java程序代碼在編譯器階段和處理器執(zhí)行階段,為了優(yōu)化執(zhí)行的效率,有可能會(huì)對(duì)指令進(jìn)行重排序。
如果一些指令彼此之間互相不影響,那么就有可能不按照代碼順序執(zhí)行,比如后面的代碼先執(zhí)行,而之前的代碼則慢執(zhí)行,但處理器會(huì)保證結(jié)束時(shí)的輸出結(jié)果是一致的。
以上的這種情況就說(shuō)明指令有可能不是有序的。
volatile變量,上面我們看過(guò)其匯編指令,會(huì)多出一條Lock前綴的指令,這條指令能夠 保證,在這條指令之前的所有指令全部執(zhí)行完畢,而在這條指令之后的所有指令全部未執(zhí)行,也相于在這里立起了一道柵欄,稱之為內(nèi)存柵欄,而更通俗的說(shuō)法,則是內(nèi)存屏障。
那么有了這道屏障,volatile變量就禁止了指令的重排序,從而保證了指令執(zhí)行的有序性。
所有對(duì)volatile變量的讀操作一定發(fā)生在對(duì)volatile變量的寫操作之后。這同時(shí)也說(shuō)明了volatile變量在多個(gè)線程之間能夠?qū)崿F(xiàn)可見性的原理。所以各種規(guī)定和操作,其實(shí)之間互有關(guān)聯(lián),彼此依賴,才能更好地保證指令執(zhí)行的準(zhǔn)確和效率。
內(nèi)存屏障
在上面我們也引出了內(nèi)存屏障的概念,也知道了,其實(shí)它就是一組處理器的操作指令。
插入一個(gè)內(nèi)存屏障,則相當(dāng)于告訴處理器和編譯器先于這個(gè)指令的必須先執(zhí)行,后于這個(gè)指令的必須后執(zhí)行。

內(nèi)存屏障另一個(gè)作用是強(qiáng)制更新一次不同CPU的緩存。
例如,一個(gè)寫屏障會(huì)把這個(gè)屏障前寫入的數(shù)據(jù)刷新到緩存,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值,而不用考慮到底是被哪個(gè)cpu核心或者哪顆CPU執(zhí)行的。
這再仔細(xì)一想,不就是上面所說(shuō)的volatile的作用嗎?
所以,內(nèi)存屏障,可見性,有序性,緩存一致性原則,在java并發(fā)中各種各樣的名詞,本質(zhì)上可能就只是同一種現(xiàn)象或者同一種設(shè)計(jì),從不同的角度觀察和探討所得出的不同的解釋。
下一篇文章
Java并發(fā)系列之synchronized