深入理解java中的volatile關鍵字

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前綴指令,這個指令相當于一個內存屏障,主要提供三個功能:

  1. 它會強制對緩存的修改立即寫入主存中
  2. 如果是寫操作,其他cpu會受到總線信號,緩存中對應的數據失效,后續再讀時,會重新從主存中讀取。
  3. 確保重排指令時,其后的指令不會被重排到內存屏障前,內存屏障前的指令也不會被排到其后,即執行內存屏障這條指令時,確保其前面的指令已經執行完畢。

volatile修飾的變量是否具有原子性

volatile只能保證讀取的變量為最新值,無法保證原子性

比如i++這個操作,因為其不是原子操作,分為讀取和++兩個操作,線程1讀取i1,如果線程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++;
    }

參考鏈接:https://www.cnblogs.com/dolphin0520/p/3920373.html

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容