四、Volatile變量的使用
volatile變量的讀寫對所有線程立即可見
只是讀和寫一步,復雜的運算不能保證對其他線程可見,因為復雜的運算可能會被編譯成多條指令,JMM只保證,volatile變量從工作內存寫回到主存是對其他線程可見的。先看一個具體的例子。
static volatile int i = 0;
// -XX:+PrintGC
// -XX:+PrintGCDetails
// -Xms20m
// -Xmn10m
// -Xmx20m
// -XX:+UseSerialGC
// -XX:MaxTenuringThreshold=15
// -XX:-HandlePromotionFailure
// -XX:+PrintHeapAtGC
public static void main(String[] args) {
int a = i++;
}1234567891011121314
編譯后的指令
JMM只是能夠保證(并不一定能夠保證,但是一條字節碼的指令也是由若干機器指令完成的,但是能夠說明問題了)getstatic 和 putstatic volatile變量的時候是原子的,至于中間的一些列操作,并不能夠保證再次期間沒有其他線程對i操作生成臟數據。也就是,JMM保證get操作的值是當前內存中最新的,以及put之后內存中i的對其他內存可見。
禁止指令的重排序
這一點,《java并發編程的藝術》一書中講的比較詳細。
重排序分為編譯器重排序和處理器重排序。為了實現volatile內存語義,JMM會分別限制這兩種類型的重排序類型。JMM針對編譯器制定的volatile重排序規則見下表。
個人總結來說,
volatile的讀下面的任何操作都不能重排序到volatile讀操作的上方,volatile上面的普通讀寫可重排序到下方。
volatile的寫上面的任何操作都不能重排序到volatile寫操作的下方,volatile下面的普通讀寫可重排序到上方。
任何兩個volatile的讀寫順序不能重排。
為了實現上述的volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。(StoreStore等屏障的介紹見文章最后)
在每個volatile寫操作的前面插入一個StoreStore屏障。
在每個volatile寫操作的后面插入一個StoreLoad屏障。
在每個volatile讀操作的后面插入一個LoadLoad屏障。
在每個volatile讀操作的后面插入一個LoadStore屏障。
插入屏障后的效果見下圖
LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。
StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經對任意處理器可見了。這是因為StoreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內存。
這里比較有意思的是,volatile寫后面的StoreLoad屏障。此屏障的作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序。因為編譯器常常無法準確判斷在一個volatile寫的后面是否需要插入一個StoreLoad屏障(比如,一個volatile寫之后方法立即return)。為了保證能正確實現volatile的內存語義,JMM在采取了保守策略:在每個volatile寫的后面,或者在每個volatile讀的前面插入一個StoreLoad屏障。從整體執行效率的角度考慮,JMM最終選擇了在每個volatile寫的后面插入一個StoreLoad屏障。因為volatile寫-讀內存語義的常見使用模式是:一個寫線程寫volatile變量,多個讀線程讀同一個volatile變量。當讀線程的數量大大超過寫線程時,選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執行效率的提升。從這里可以看到JMM在實現上的一個特點:首先確保正確性,然后再去追求執行效率。
上面這段話引自《java并發編程的藝術》一書,但是不是很明白,volatile的寫前面的所有操作都不得拍到volatile寫之后,為什么這里只加入了Store-Store屏障呢,這樣普通讀不就可以重拍到volatile寫的下方了??????
如果,volatile讀的上面還有volatile讀,因為volatile讀下面都會插入load-load屏障,所以兩者不會重排。如果volatile讀的上面還有volatile寫,volatile寫后面加入了store-load,所以下面的volatile讀不能能與之重排序。
屏障介紹:
評論
查看更多