在多線程并發(fā)編程中,鎖的運用很常見。synchronized 的幾種運用方式,相信大部分 Java 程序員已經(jīng)很熟悉。而 volatile 作為輕量級的 synchronized,不會像鎖一樣造成阻塞,因此,在能夠安全使用 volatile 的情況下,volatile 可以提供一些優(yōu)于鎖的可伸縮特性。如果讀操作的次數(shù)要遠(yuǎn)遠(yuǎn)超過寫操作,與鎖相比,volatile 變量通常能夠減少同步的性能開銷。
在現(xiàn)代計算機(jī)系統(tǒng)中,由于計算機(jī)的存儲設(shè)備與處理器的運算速度有幾個數(shù)量級的差距,所以現(xiàn)代計算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存來作為內(nèi)存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運算能快速進(jìn)行,當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中,這樣處理器就無須等待緩存的內(nèi)存讀寫了。
下面是計算機(jī)系統(tǒng)中處理器、高速緩存、主內(nèi)存間的交互關(guān)系:

基于高速緩存的存儲交互很好的解決了處理器與內(nèi)存的速度矛盾,但是也為計算機(jī)系統(tǒng)帶來更高的復(fù)雜度,因為它引入了一個新的問題:緩存一致性。
下面是Java中線程、主內(nèi)存、工作內(nèi)存交互關(guān)系:

Volatile 的官方定義
Java 語言規(guī)范第三版中對 volatile 的定義如下: java編程語言允許線程訪問共享變量,為了確保共享變量能被準(zhǔn)確和一致的更新,線程應(yīng)該確保通過排他鎖單獨獲得這個變量。Java語言提供了 volatile,在某些情況下比鎖更加方便。如果一個字段被聲明成volatile,java線程內(nèi)存模型確保所有線程看到這個變量的值是一致的。
內(nèi)存不可見的含義
在 JVM 中,對于多線程應(yīng)用,如果多個線程同時使用某個沒有 volatile 修飾的變量時,每個線程會從主內(nèi)存拷貝目標(biāo)變量到當(dāng)前線程的工作內(nèi)存中,然后在各自的工作內(nèi)存進(jìn)行具體的操作。
可見性的定義:可見性是指當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得到這個修改。
在上面的情景中,不同線程的對主內(nèi)存變量副本的操作不能夠即時的反饋到主內(nèi)存區(qū),其他線程的工作內(nèi)存更是無法感知,內(nèi)存不可見。
如何保證內(nèi)存可見
volatile 如何實現(xiàn)內(nèi)存可見的呢?
在x86處理器下通過工具獲取JIT編譯器生成的匯編指令:
| 語言 | 代碼片段 |
|---|---|
| Java | instance = new Singleton(); </br> //instance 是 volatile 修飾變量 |
| 匯編 | 0x01a3de1d: movb $0x0,0x1104800(%esi);</br>0x01a3de24: lock addl $0x0,(%esp); |
有 volatile 變量修飾的共享變量進(jìn)行寫操作的時候會多第二行匯編代碼,通過查IA-32架構(gòu)軟件開發(fā)者手冊可知,lock前綴的指令在多核處理器下會引發(fā)了兩件事情。
- 將當(dāng)前處理器緩存行的數(shù)據(jù)回寫到系統(tǒng)內(nèi)存。
- 這個回寫內(nèi)存的操作會引起在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。
也就是說,處理器為了提高處理速度,不直接和內(nèi)存通訊,而是先將內(nèi)存數(shù)據(jù)拷貝到緩存后再操作(同上圖)。如果變量聲明了 volatile,那么處理器讀取操作會直接和內(nèi)存進(jìn)行通訊,將變量所在緩存行的數(shù)據(jù)直接寫入系統(tǒng)內(nèi)存或者直接讀取系統(tǒng)內(nèi)存數(shù)據(jù)。但是如果其他處理器緩存的數(shù)據(jù)仍然是舊的數(shù)據(jù),那么再執(zhí)行計算操作就是無意義的。所以這里就存在緩存一致性協(xié)議,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢測自身緩存是否過期,如果檢測到自己緩存行對應(yīng)的數(shù)據(jù)被修改,那么會將當(dāng)前處理器緩存行設(shè)置為無效狀態(tài)?。當(dāng)處理器需要該數(shù)據(jù)進(jìn)行操作時,會強(qiáng)制從系統(tǒng)內(nèi)存重新加載到當(dāng)前處理器緩存中。
緩存一致性機(jī)制會阻止同時修改被兩個以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)。
具體的專有名詞及細(xì)節(jié)可以看文末的 reference(本節(jié)內(nèi)容摘錄自文末的參考文章).
保證對 64 位變量讀寫的原子性
JVM 可以保證對 32位 數(shù)據(jù)讀寫的原子性,但是對于 long 和 double 這樣 64位 的數(shù)據(jù)的讀寫,會將其分為 高32位 和 低32位 分兩次讀寫。所以對于 long 或 double 的讀寫并不是原子性的,這樣在并發(fā)程序中共享 long 或 double 變量就可能會出現(xiàn)問題,于是 JVM 提供了 volatile 關(guān)鍵字來解決這個問題:
使用 volatile 修飾的 long 或 double 變量,JVM 可以保證對其讀寫的原子性。
但是,此處的 “寫” 僅指對 64位 的變量進(jìn)行直接賦值。
指令重新排序?qū)?volatile 的影響
如果一個操作不是原子操作,那么 JVM 便可能會對該操作涉及的指令進(jìn)行 重排序優(yōu)化。重排序即在不改變程序語義的前提下,通過調(diào)整指令的執(zhí)行順序,盡可能達(dá)到提高運行效率的目的。
int a = 1;
int b = 2;
a++;
b++;
可能會被重新排序為:
int a = 1;
a++;
int b = 2;
b++;
這樣看是沒什么影響的。
但當(dāng)一個變量是 volatile 修飾時,指令重排序就可能會出現(xiàn)問題。
public class Counter {
private int numA;
private int numB
private volatile int numC;
public void update(int numA, int numB, int numC){
this.numA = numA;
this.numB = numB;
this.numC = numC;
}
}
當(dāng) update 方法調(diào)用時,numA,numB,numC 的新值都會直接寫入系統(tǒng)內(nèi)存。但是如果重新排序成這樣:
public void update(int numA, int numB, int numC){
this.numC = numC;
this.numA = numA;
this.numB = numB;
}
修改 numC 變量時,A和B的值仍會寫入主內(nèi)存,但這一次是在A和B的新值寫入之前發(fā)生的。因此,其他線程無法正確地看到A和B的新值。重新排序的指令的語義已經(jīng)改變。
為了解決指令重新排序這個難題,Java volatile 關(guān)鍵字除了提供可見性保證之外,還提供“happens-before”保證:
如果讀取/寫入其他變量的操作最初就發(fā)生在寫入 volatile 修飾變量之前,那么指令重新排序時,不允許這個操作被排到被 volatile 修飾的變量寫入之后;注意,對于其他變量的操作最初發(fā)生在寫入 volatile 修飾變量之后的,那么重新排序是仍然有可能排到 volatile 修飾變量寫入之前。
如果讀取/寫入其他變量的操作最初就發(fā)生在寫入 volatile 修飾變量之后,那么指令重新排序時,不允許這個操作被排到被 volatile 修飾的變量寫入之前;注意,對于其他變量的操作最初發(fā)生在寫入 volatile 修飾變量之前的,那么重新排序是仍然有可能排到 volatile 修飾變量寫入之后。
上述的“happens-before”保證正在被實施。
必須保證操作原子性
對 volatile 修飾的變量操作時,即使每次都是從系統(tǒng)內(nèi)存讀取,都是直接寫入系統(tǒng)內(nèi)存,仍然會存在問題。
當(dāng)多個線程同時寫入一個 volatile 變量時,例如 i++ 操作。對于 i++ 這個語句,事實上涉及了 讀?。薷模瓕懭?三個操作:
- 讀取變量到棧中某個位置
- 對棧中該位置的值進(jìn)行自增
- 將自增后的值寫回到變量對應(yīng)的存儲位置
volatile 變量只能保證可見性,在不符合以下兩條規(guī)則的運算場景中,仍需要通過加鎖(使用 synchronized 或 java.util.concurrent 中的原子類)來保證原子性。
- 運算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
- 變量不需要與其他的狀態(tài)變量共同參與不變約束。
合適的使用場景
讀取和寫入一個 volatile 變量會直接和系統(tǒng)內(nèi)存通信,對比與處理器緩存通信的消耗要大得多。訪問 volatile 變量還防止指令重新排序,這是一種正常的性能增強(qiáng)技術(shù)。所以只有在真正需要變量強(qiáng)制可見性時才應(yīng)該使用。
具體的幾種場景可以參考正確使用 Volatile 變量
參考資料: