并發(fā)關(guān)鍵字volatile(重排序和內(nèi)存屏障)

Java 語(yǔ)言中的volatile變量可以被看作是一種 “程度較輕的 synchronized”;synchronized 塊相比,volatile 變量所需的編碼較少,并且運(yùn)行時(shí)開(kāi)銷也較少,但是它所能實(shí)現(xiàn)的功能也僅是 synchronized 的一部分。

鎖和volatile

鎖提供了兩種主要特性:原子性和可見(jiàn)性。

原子性即一次只允許一個(gè)線程持有某個(gè)特定的鎖,一次就只有一個(gè)線程能夠使用共享數(shù)據(jù)。可見(jiàn)性是必須確保釋放鎖之前對(duì)共享數(shù)據(jù)做出的更改對(duì)于隨后獲得該鎖的另一個(gè)線程是可見(jiàn)的 。

Volatile 變量具有 synchronized 的可見(jiàn)性特性,但是不具備原子特性。
當(dāng)一個(gè)變量定義為 volatile 之后,將具備:

1.保證此變量對(duì)所有的線程的可見(jiàn)性,當(dāng)一個(gè)線程修改了這個(gè)變量的值,volatile 保證了新值能立即同步到主內(nèi)存,其它線程每次使用前立即從主內(nèi)存刷新。但普通變量做不到這點(diǎn),普通變量的值在線程間傳遞均需要通過(guò)主內(nèi)存來(lái)完成。
2.禁止指令重排序優(yōu)化。有volatile修飾的變量,賦值后多執(zhí)行了一個(gè)“l(fā)oad addl $0x0, (%esp)”操作,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障(指令重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置)。

我們通過(guò)有序性來(lái)詳細(xì)看下指令重排序。

指令重排序

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

int i = 0;              
boolean flag = false;
i = 1;                //語(yǔ)句1  
flag = true;          //語(yǔ)句2

從代碼順序上看,語(yǔ)句1是在語(yǔ)句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會(huì)保證語(yǔ)句1一定會(huì)在語(yǔ)句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會(huì)發(fā)生指令重排序(Instruction Reorder)。
指令重排序:

一般來(lái)說(shuō),處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語(yǔ)句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。

比如上面的代碼中,語(yǔ)句1和語(yǔ)句2誰(shuí)先執(zhí)行對(duì)最終的程序結(jié)果并沒(méi)有影響,那么就有可能在執(zhí)行過(guò)程中,語(yǔ)句2先執(zhí)行而語(yǔ)句1后執(zhí)行。雖然處理器會(huì)對(duì)指令進(jìn)行重排序,但是它會(huì)保證程序最終結(jié)果會(huì)和代碼順序執(zhí)行結(jié)果相同,那么它靠什么保證的呢?靠的是數(shù)據(jù)依賴性:

編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。

舉例如下代碼

double pi  = 3.14;    //A  
double r   = 1.0;     //B  
double area = pi * r * r; //C  

上面三個(gè)操作的數(shù)據(jù)依賴關(guān)系如下圖所示

20151008235938677.png

A和C之間存在數(shù)據(jù)依賴關(guān)系,同時(shí)B和C之間也存在數(shù)據(jù)依賴關(guān)系。因此在最終執(zhí)行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結(jié)果將會(huì)被改變)。但A和B之間沒(méi)有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序A和B之間的執(zhí)行順序。下圖是該程序的兩種執(zhí)行順序:


20151009000030517.png

在計(jì)算機(jī)中,軟件技術(shù)和硬件技術(shù)有一個(gè)共同的目標(biāo):在不改變程序執(zhí)行結(jié)果的前提下,盡可能的開(kāi)發(fā)并行度。編譯器和處理器都遵從這一目標(biāo)。
這里所說(shuō)的數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作,在單線程程序中,對(duì)存在控制依賴的操作重排序,不會(huì)改變執(zhí)行結(jié)果;但在多線程程序中,對(duì)存在控制依賴的操作重排序,可能會(huì)改變程序的執(zhí)行結(jié)果。這是就需要內(nèi)存屏障來(lái)保證可見(jiàn)性了。

內(nèi)存屏障

內(nèi)存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障。
內(nèi)存屏障有兩個(gè)作用:

1.阻止屏障兩側(cè)的指令重排序;
2.強(qiáng)制把寫緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫回主內(nèi)存,讓緩存中相應(yīng)的數(shù)據(jù)失效。

  • 對(duì)于Load Barrier來(lái)說(shuō),在指令前插入Load Barrier,可以讓高速緩存中的數(shù)據(jù)失效,強(qiáng)制從新從主內(nèi)存加載數(shù)據(jù);
  • 對(duì)于Store Barrier來(lái)說(shuō),在指令后插入Store Barrier,能讓寫入緩存中的最新數(shù)據(jù)更新寫入主內(nèi)存,讓其他線程可見(jiàn)。

java的內(nèi)存屏障通常所謂的四種即LoadLoad,StoreStore,LoadStore,StoreLoad實(shí)際上也是上述兩種的組合,完成一系列的屏障和數(shù)據(jù)同步功能。

LoadLoad屏障:對(duì)于這樣的語(yǔ)句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問(wèn)前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreStore屏障:對(duì)于這樣的語(yǔ)句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對(duì)其它處理器可見(jiàn)。
LoadStore屏障:對(duì)于這樣的語(yǔ)句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreLoad屏障:對(duì)于這樣的語(yǔ)句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對(duì)所有處理器可見(jiàn)。它的開(kāi)銷是四種屏障中最大的。在大多數(shù)處理器的實(shí)現(xiàn)中,這個(gè)屏障是個(gè)萬(wàn)能屏障,兼具其它三種內(nèi)存屏障的功能

volatile的內(nèi)存屏障策略非常嚴(yán)格保守,非常悲觀且毫無(wú)安全感的心態(tài):

在每個(gè)volatile寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障;
在每個(gè)volatile讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障;

由于內(nèi)存屏障的作用,避免了volatile變量和其它指令重排序、線程之間實(shí)現(xiàn)了通信,使得volatile表現(xiàn)出了鎖的特性。

volatile 性能:

volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來(lái)保證處理器不發(fā)生亂序執(zhí)行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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