深入理解java中的volatile關(guān)鍵字

volatile 美[?vɑ?l?tl]
adj. 易變的; 無(wú)定性的; 無(wú)常性的; 可能急劇波動(dòng)的; 不穩(wěn)定的; 易惡化的; 易揮發(fā)的; 易發(fā)散的;

語(yǔ)義

保證任何一個(gè)線程改變了volatile修飾的變量,這個(gè)改動(dòng)對(duì)其他線程都是可見(jiàn)的

禁止該條指令重排序

線程間可見(jiàn)

看下下面一段代碼:

//線程1
boolean stop = false;
while(!stop){
    doSomething();
}
//線程2
stop = true;

這種情況下,線程2對(duì)stop的修改可能不會(huì)影響線程1的運(yùn)行,也可能會(huì)影響線程1的運(yùn)行;
假設(shè)線程1和線程2是在不同的處理器中運(yùn)行,線程2修改了stop的值,按照現(xiàn)代處理器的設(shè)計(jì)思路,只會(huì)改動(dòng)cpu緩存cache中的值,過(guò)了一段時(shí)間后才會(huì)同步到主存中;對(duì)于線程2同樣讀取的stop值也是讀的其cache中的值,所以兩個(gè)線程的stop何時(shí)同步就變得不確定。volatile就是為了解決此類(lèi)問(wèn)題而設(shè)計(jì),后面會(huì)詳細(xì)說(shuō)明怎么解決的。

什么是指令重排序

為了盡可能減少內(nèi)存操作速度遠(yuǎn)慢于CPU運(yùn)行速度所帶來(lái)的CPU空置的影響,虛擬機(jī)會(huì)按照自己的一些規(guī)則(這規(guī)則后面再敘述)將程序編寫(xiě)順序打亂——即寫(xiě)在后面的代碼在時(shí)間順序上可能會(huì)先執(zhí)行,而寫(xiě)在前面的代碼會(huì)后執(zhí)行——以盡可能充分地利用CPU。比方說(shuō)new一個(gè)byte[1024*1024]的數(shù)組,其后的操作可能不等其地址分配完畢前就執(zhí)行。
但是,不管怎么重排序,單線程的執(zhí)行結(jié)果肯定是和程序順序一致的

public void execute(){
    int a=0;
    int b=1;
    int c=a+b;
}

不管a=0還是b=1是什么順序,c=a+b肯定在這兩個(gè)語(yǔ)句之后。虛擬機(jī)有一套重排序的規(guī)則,保證這個(gè)結(jié)果。
重排序,總結(jié)下來(lái)就是,單個(gè)線程里看所有操作都是有序的,但是看別的線程,操作總是亂七八糟的。
volatile聲明的變量會(huì)確保本變量前的語(yǔ)句均被執(zhí)行

//線程1:
context = loadContext();   //語(yǔ)句1
//如果inited不被volatile修飾,可能因?yàn)檎Z(yǔ)句2優(yōu)先執(zhí)行,導(dǎo)致線程2報(bào)錯(cuò),加了volatile修飾符后,語(yǔ)句2處理前,語(yǔ)句1肯定已經(jīng)執(zhí)行完畢,不會(huì)出現(xiàn)這個(gè)問(wèn)題。
volatile inited = true;             //語(yǔ)句2

//線程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

實(shí)現(xiàn)原理

volatile修飾的變量編譯后,會(huì)在變量前加個(gè)lock前綴指令,這個(gè)指令相當(dāng)于一個(gè)內(nèi)存屏障,主要提供三個(gè)功能:

  1. 它會(huì)強(qiáng)制對(duì)緩存的修改立即寫(xiě)入主存中
  2. 如果是寫(xiě)操作,其他cpu會(huì)受到總線信號(hào),緩存中對(duì)應(yīng)的數(shù)據(jù)失效,后續(xù)再讀時(shí),會(huì)重新從主存中讀取。
  3. 確保重排指令時(shí),其后的指令不會(huì)被重排到內(nèi)存屏障前,內(nèi)存屏障前的指令也不會(huì)被排到其后,即執(zhí)行內(nèi)存屏障這條指令時(shí),確保其前面的指令已經(jīng)執(zhí)行完畢。

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

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

比如i++這個(gè)操作,因?yàn)槠洳皇窃硬僮鳎譃樽x取和++兩個(gè)操作,線程1讀取i1,如果線程1被阻塞住,沒(méi)來(lái)得及++;線程2讀取的也是最新的,結(jié)果也是1,線程2更新i后,i變?yōu)?code>2,線程1繼續(xù)運(yùn)行,由于其工作內(nèi)存中的i還是1,所以導(dǎo)致線程1會(huì)再將i更新為2,這樣就出現(xiàn)問(wèn)題。

使用場(chǎng)景

synchronized本質(zhì)是加鎖,對(duì)性能肯定有影響。所以某些情況下volatile關(guān)鍵字性能是優(yōu)于synchronized的;但是volatile無(wú)法保證原子性,所以無(wú)法替代syschronized。
一般在如下場(chǎng)景下使用volatile關(guān)鍵字:
  1)對(duì)變量的寫(xiě)操作不依賴于當(dāng)前值
  2)該變量沒(méi)有包含在具有其他變量的不變式中

下面列舉幾個(gè)Java中使用volatile的幾個(gè)場(chǎng)景。
1.狀態(tài)標(biāo)記量

volatile boolean shutdownRequested;
 
...
 
public void shutdown() { 
    shutdownRequested = true; 
}
 
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

2.獨(dú)立觀察(independent observation)
定期 “發(fā)布” 觀察結(jié)果供程序內(nèi)部使用

//身份驗(yàn)證機(jī)制如何記憶最近一次登錄的用戶的名字
public class UserManager {
    public volatile String lastUser; //發(fā)布的信息
 
    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.開(kāi)銷(xiāo)較低的“讀-寫(xiě)鎖”策略
如果讀操作遠(yuǎn)遠(yuǎn)超過(guò)寫(xiě)操作,您可以結(jié)合使用內(nèi)部鎖和 volatile 變量來(lái)減少公共代碼路徑的開(kāi)銷(xiāo)。

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;
 
    //讀操作,沒(méi)有synchronized,提高性能
    public int getValue() { 
        return value; 
    } 
 
    //寫(xiě)操作,必須synchronized。因?yàn)閤++不是原子操作
    public synchronized int increment() {
        return value++;
    }

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容