volatile 美[?vɑ?l?tl]
adj. 易變的; 無定性的; 無常性的; 可能急劇波動的; 不穩定的; 易惡化的; 易揮發的; 易發散的;
語義
保證任何一個線程改變了volatile修飾的變量,這個改動對其他線程都是可見的
禁止該條指令重排序
線程間可見
看下下面一段代碼:
//線程1
boolean stop = false;
while(!stop){
doSomething();
}
//線程2
stop = true;
這種情況下,線程2對stop的修改可能不會影響線程1的運行,也可能會影響線程1的運行;
假設線程1和線程2是在不同的處理器中運行,線程2修改了stop的值,按照現代處理器的設計思路,只會改動cpu緩存cache中的值,過了一段時間后才會同步到主存中;對于線程2同樣讀取的stop值也是讀的其cache中的值,所以兩個線程的stop何時同步就變得不確定。volatile就是為了解決此類問題而設計,后面會詳細說明怎么解決的。
什么是指令重排序
為了盡可能減少內存操作速度遠慢于CPU運行速度所帶來的CPU空置的影響,虛擬機會按照自己的一些規則(這規則后面再敘述)將程序編寫順序打亂——即寫在后面的代碼在時間順序上可能會先執行,而寫在前面的代碼會后執行——以盡可能充分地利用CPU。比方說new一個byte[1024*1024]的數組,其后的操作可能不等其地址分配完畢前就執行。
但是,不管怎么重排序,單線程的執行結果肯定是和程序順序一致的
public void execute(){
int a=0;
int b=1;
int c=a+b;
}
不管a=0還是b=1是什么順序,c=a+b肯定在這兩個語句之后。虛擬機有一套重排序的規則,保證這個結果。
重排序,總結下來就是,單個線程里看所有操作都是有序的,但是看別的線程,操作總是亂七八糟的。
volatile聲明的變量會確保本變量前的語句均被執行
//線程1:
context = loadContext(); //語句1
//如果inited不被volatile修飾,可能因為語句2優先執行,導致線程2報錯,加了volatile修飾符后,語句2處理前,語句1肯定已經執行完畢,不會出現這個問題。
volatile inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
實現原理
volatile修飾的變量編譯后,會在變量前加個lock前綴指令,這個指令相當于一個內存屏障,主要提供三個功能:
- 它會強制對緩存的修改立即寫入主存中
- 如果是寫操作,其他cpu會受到總線信號,緩存中對應的數據失效,后續再讀時,會重新從主存中讀取。
- 確保重排指令時,其后的指令不會被重排到內存屏障前,內存屏障前的指令也不會被排到其后,即執行內存屏障這條指令時,確保其前面的指令已經執行完畢。
volatile修飾的變量是否具有原子性
volatile只能保證讀取的變量為最新值,無法保證原子性
比如i++
這個操作,因為其不是原子操作,分為讀取和++
兩個操作,線程1讀取i
為1
,如果線程1被阻塞住,沒來得及++
;線程2讀取的也是最新的,結果也是1
,線程2更新i
后,i
變為2
,線程1繼續運行,由于其工作內存中的i
還是1,所以導致線程1會再將i
更新為2,這樣就出現問題。
使用場景
synchronized本質是加鎖,對性能肯定有影響。所以某些情況下volatile關鍵字性能是優于synchronized的;但是volatile無法保證原子性,所以無法替代syschronized。
一般在如下場景下使用volatile關鍵字:
1)對變量的寫操作不依賴于當前值
2)該變量沒有包含在具有其他變量的不變式中
下面列舉幾個Java中使用volatile的幾個場景。
1.狀態標記量
volatile boolean shutdownRequested;
...
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
2.獨立觀察(independent observation)
定期 “發布” 觀察結果供程序內部使用
//身份驗證機制如何記憶最近一次登錄的用戶的名字
public class UserManager {
public volatile String lastUser; //發布的信息
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}
3.開銷較低的“讀-寫鎖”策略
如果讀操作遠遠超過寫操作,您可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
//讀操作,沒有synchronized,提高性能
public int getValue() {
return value;
}
//寫操作,必須synchronized。因為x++不是原子操作
public synchronized int increment() {
return value++;
}