一、volatile概念
談到volatile,理解原子性和易變性是不同的概念這一點(diǎn)很重要,volatile是輕量級(jí)的鎖,它只具備可見性,但沒有原子特性。如果你將一個(gè)域聲明為volatile,那么只要對(duì)這個(gè)域產(chǎn)生了寫操作,所有的讀操作都可以看到這個(gè)修改,即便使用了本地緩存也一樣,volatile會(huì)被立即寫入到主內(nèi)存中,而讀的操作就發(fā)生在主內(nèi)存中。在非volatile域上的原子操作不必刷新到主內(nèi)存中,所以讀操作的任務(wù)看不到這個(gè)值,如果多個(gè)任務(wù)在同時(shí)訪問某個(gè)域,那么這個(gè)域就應(yīng)該是volatile,否則這個(gè)域就應(yīng)該經(jīng)過同步來訪問,同步也會(huì)導(dǎo)致向主內(nèi)存中刷新。
使用volatile而不是synchronize的唯一安全的情況就是類中只有一個(gè)可變的域。個(gè)人認(rèn)為,第一選擇應(yīng)該是synchronize,這應(yīng)該最安全的方式 。如果并發(fā)水平不高,最好還是不要使用。
二、volatile變量的使用
在使用volatile變量時(shí),應(yīng)當(dāng)考慮是否滿足下面這樣的要求:
對(duì)變量的寫入操作不依賴變量的當(dāng)前值
說白了volatile 變量不能用作線程安全計(jì)數(shù)器,類似于i++這種增量操作。增量操作符++不是原子的。這個(gè)操作分解開來看是先從堆內(nèi)存中獲得i值的副本放到緩存中,然后對(duì)副本值加1,最后再將副本值寫回到堆內(nèi)存的變量i中,是一個(gè)由讀取-修改-寫入操作序列組成的組合操作。
沒有用于其它變量的不變式條件中(lower小于upper)
/*
* volatile只保證lower與upper的最后寫入一定會(huì)被其它讀取的線程看到
* 但不能保證在lower或upper寫入時(shí),另一個(gè)變量的值沒有發(fā)生變化
*/
private volatile int lower;
private volatile int upper;
public int getLower() {
return lower;
}
public int getUpper() {
return upper;
}
public void setLower(int lower) {
if (lower 》 upper)
throw new IllegalArgumentException();
this.lower = lower;
}
public void setUpper(int upper) {
if (upper 《 lower)
throw new IllegalArgumentException();
this.upper = upper;
}1234567891011121314151617181920212223242526
volatile變量就不適合用于不變性條件這種情況,以上下限為例,lower必須小于upper,這就是一種不變性條件,你可以理解為這是一種規(guī)則限制。它們都只有set與get方法,但在set方法里面加入了約束條件,這時(shí),volatile的可見性就不能保證并發(fā)時(shí),lower與upper之間的不變性條件(lower小于upper)一定成立了。
曾經(jīng)見到過這樣的一個(gè)面試題:
volatile 能使得一個(gè)非原子操作變成原子操作嗎?
答案是能的,在基本數(shù)據(jù)類型中l(wèi)ong和double是非原子性的,double 和 long 都是64位寬,因此對(duì)這兩種類型的讀是分為兩部分的,第一次讀取第一個(gè) 32 位,然后再讀剩下的 32 位,如果一個(gè)線程正在修改該 long 變量的值,另一個(gè)線程可能只能看到該值的一半(前 32 位)。但是將long與double加上volatile ,對(duì)變量的讀寫是原子性的
三、volatile的作用
volatile不是保護(hù)線程安全的。它保護(hù)的是變量安全。主要的功能是保護(hù)變量不被主函數(shù)和中斷函數(shù)反復(fù)修改造成讀寫錯(cuò)誤。
volatile具備兩種特性:
保證此變量對(duì)所有線程的可見性,指一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程來說是可見的,但并不是多線程安全的。
禁止指令重排序優(yōu)化。
Volatile和Synchronized四個(gè)不同點(diǎn):
1 粒度不同,后者鎖對(duì)象和類,前者針對(duì)變量
2 syn阻塞,volatile線程不阻塞
3 syn保證三大特性,volatile不保證原子性
4 syn編譯器優(yōu)化,volatile不優(yōu)化
四、Volatile變量的使用
volatile變量的讀寫對(duì)所有線程立即可見
只是讀和寫一步,復(fù)雜的運(yùn)算不能保證對(duì)其他線程可見,因?yàn)閺?fù)雜的運(yùn)算可能會(huì)被編譯成多條指令,JMM只保證,volatile變量從工作內(nèi)存寫回到主存是對(duì)其他線程可見的。先看一個(gè)具體的例子。
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只是能夠保證(并不一定能夠保證,但是一條字節(jié)碼的指令也是由若干機(jī)器指令完成的,但是能夠說明問題了)getstatic 和 putstatic volatile變量的時(shí)候是原子的,至于中間的一些列操作,并不能夠保證再次期間沒有其他線程對(duì)i操作生成臟數(shù)據(jù)。也就是,JMM保證get操作的值是當(dāng)前內(nèi)存中最新的,以及put之后內(nèi)存中i的對(duì)其他內(nèi)存可見。
禁止指令的重排序
這一點(diǎn),《java并發(fā)編程的藝術(shù)》一書中講的比較詳細(xì)。
重排序分為編譯器重排序和處理器重排序。為了實(shí)現(xiàn)volatile內(nèi)存語義,JMM會(huì)分別限制這兩種類型的重排序類型。JMM針對(duì)編譯器制定的volatile重排序規(guī)則見下表。
個(gè)人總結(jié)來說,
volatile的讀下面的任何操作都不能重排序到volatile讀操作的上方,volatile上面的普通讀寫可重排序到下方。
volatile的寫上面的任何操作都不能重排序到volatile寫操作的下方,volatile下面的普通讀寫可重排序到上方。
任何兩個(gè)volatile的讀寫順序不能重排。
為了實(shí)現(xiàn)上述的volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。(StoreStore等屏障的介紹見文章最后)
在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
插入屏障后的效果見下圖
LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。
StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對(duì)任意處理器可見了。這是因?yàn)镾toreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。
這里比較有意思的是,volatile寫后面的StoreLoad屏障。此屏障的作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序。因?yàn)榫幾g器常常無法準(zhǔn)確判斷在一個(gè)volatile寫的后面是否需要插入一個(gè)StoreLoad屏障(比如,一個(gè)volatile寫之后方法立即return)。為了保證能正確實(shí)現(xiàn)volatile的內(nèi)存語義,JMM在采取了保守策略:在每個(gè)volatile寫的后面,或者在每個(gè)volatile讀的前面插入一個(gè)StoreLoad屏障。從整體執(zhí)行效率的角度考慮,JMM最終選擇了在每個(gè)volatile寫的后面插入一個(gè)StoreLoad屏障。因?yàn)関olatile寫-讀內(nèi)存語義的常見使用模式是:一個(gè)寫線程寫volatile變量,多個(gè)讀線程讀同一個(gè)volatile變量。當(dāng)讀線程的數(shù)量大大超過寫線程時(shí),選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。從這里可以看到JMM在實(shí)現(xiàn)上的一個(gè)特點(diǎn):首先確保正確性,然后再去追求執(zhí)行效率。
上面這段話引自《java并發(fā)編程的藝術(shù)》一書,但是不是很明白,volatile的寫前面的所有操作都不得拍到volatile寫之后,為什么這里只加入了Store-Store屏障呢,這樣普通讀不就可以重拍到volatile寫的下方了??????
如果,volatile讀的上面還有volatile讀,因?yàn)関olatile讀下面都會(huì)插入load-load屏障,所以兩者不會(huì)重排。如果volatile讀的上面還有volatile寫,volatile寫后面加入了store-load,所以下面的volatile讀不能能與之重排序。
屏障介紹:
評(píng)論
查看更多