深入剖析volatile關(guān)鍵字

1.并發(fā)編程中的三個概念

在并發(fā)編程中,我們通常會遇到以下三個問題:原子性問題,可見性問題,有序性問題厂镇。我們先看具體看一下這三個概念:

原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行左刽。

可見性:是指當(dāng)多個線程訪問同一個變量時捺信,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值悠反。

//線程1執(zhí)行的代碼
int i = 0;
i = 10;
//線程2執(zhí)行的代碼
j = I;

假若執(zhí)行線程1的是CPU1残黑,執(zhí)行線程2的是CPU2馍佑。由上面的分析可知,當(dāng)線程1執(zhí)行 i =10這句時梨水,會先把i的初始值加載到CPU1的高速緩存中拭荤,然后賦值為10,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了疫诽,卻沒有立即寫入到主存當(dāng)中舅世。此時線程2執(zhí)行 j = i,它會先去主存讀取i的值并加載到CPU2的緩存當(dāng)中奇徒,注意此時內(nèi)存當(dāng)中i的值還是0雏亚,那么就會使得j的值為0,而不是10.

這就是可見性問題摩钙,線程1對變量i修改了之后罢低,線程2沒有立即看到線程1修改的值。

有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行胖笛。

//線程1:
context = loadContext();   //語句1
inited = true;             //語句2
//線程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代碼中网持,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會被重排序长踊。假如發(fā)生了重排序功舀,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會以為初始化工作已經(jīng)完成身弊,那么就會跳出while循環(huán)辟汰,去執(zhí)行doSomethingwithconfig(context)方法,而此時context并沒有被初始化阱佛,就會導(dǎo)致程序出錯帖汞。
從上面可以看出,指令重排序不會影響單個線程的執(zhí)行瘫絮,但是會影響到線程并發(fā)執(zhí)行的正確性涨冀。也就是說,要想并發(fā)程序正確地執(zhí)行麦萤,必須要保證原子性、可見性以及有序性扁眯。只要有一個沒有被保證壮莹,就有可能會導(dǎo)致程序運行不正確。

2.volatile關(guān)鍵字的兩層語義

一旦一個共享變量(類的成員變量姻檀、類的靜態(tài)成員變量)被volatile修飾之后命满,那么就具備了兩層語義:

  • 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值绣版,這新值對其他線程來說是立即可見的胶台。
  • 禁止進行指令重排序歼疮。

假如線程1先執(zhí)行,線程2后執(zhí)行:

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

使用volatile修飾之后:

  • 1诈唬、使用volatile關(guān)鍵字會強制將修改的值立即寫入主存韩脏;

  • 2、使用volatile關(guān)鍵字的話铸磅,當(dāng)線程2進行修改時赡矢,會導(dǎo)致線程1的工作內(nèi)存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應(yīng)的緩存行無效)阅仔;

  • 3吹散、由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取八酒。

那么在線程2修改stop值時(當(dāng)然這里包括2個操作空民,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存)羞迷,會使得線程1的工作內(nèi)存中緩存變量stop的緩存行無效界轩,然后線程1讀取時,發(fā)現(xiàn)自己的緩存行無效闭树,它會等待緩存行對應(yīng)的主存地址被更新之后耸棒,然后去對應(yīng)的主存讀取最新的值柒啤。線程1讀取到的就是最新的正確的值婚温。

3.volatile保證內(nèi)存可見性

對于可見性,Java提供了volatile關(guān)鍵字來保證可見性眼溶。

當(dāng)一個共享變量被volatile修飾時碍现,它會保證修改的值會立即被更新到主存幅疼,當(dāng)有其他線程需要讀取時,它會去內(nèi)存中讀取新值昼接。

而普通的共享變量不能保證可見性爽篷,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的慢睡,當(dāng)其他線程去讀取時逐工,此時內(nèi)存中可能還是原來的舊值,因此無法保證可見性漂辐。

另外泪喊,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼髓涯,并且在釋放鎖之前會將對變量的修改刷新到主存當(dāng)中袒啼。因此可以保證可見性。

image.png

4.volatile禁止指令重排

volatile關(guān)鍵字提供內(nèi)存屏障的方式來防止指令被重排,編譯器在生成字節(jié)碼文件時蚓再,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序滑肉。

在Java內(nèi)存模型中說過,為了性能優(yōu)化摘仅,編譯器和處理器會進行指令重排序靶庙;也就是說java程序天然的有序性可以總結(jié)為:如果在本線程內(nèi)觀察,所有的操作都是有序的实檀;如果在一個線程觀察另一個線程惶洲,所有的操作都是無序的。在單例模式的實現(xiàn)上有一種雙重檢驗鎖定的方式(Double-checked Locking)膳犹。代碼如下:

public class Singleton {
    private Singleton() { }
    private volatile static Singleton instance;
    public Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

這里為什么要加volatile了恬吕?我們先來分析一下不加volatile的情況,有問題的語句是這條:instance = new Singleton();
這條語句實際上包含了三個操作:

  • 1.分配對象的內(nèi)存空間须床;
  • 2.初始化對象铐料;
  • 3.設(shè)置instance指向剛分配的內(nèi)存地址。但由于存在重排序的問題豺旬,可能有以下的執(zhí)行順序:
image

如果2和3進行了重排序的話钠惩,線程B進行判斷if(instance==null)時就會為true,而實際上這個instance并沒有初始化成功族阅,顯而易見對線程B來說之后的操作就會是錯的篓跛。而用volatile修飾的話就可以禁止2和3操作重排序,從而避免這種情況坦刀。volatile包含禁止指令重排序的語義愧沟,其具有有序性。

通過生成匯編代碼鲤遥,可以清晰的看到加入volatile和未加入volatile的差別沐寺。volatile變量修飾的共享變量,在進行寫操作的時候會多出一個lock前綴的匯編指令盖奈,這個指令會觸發(fā)總線鎖或者緩存鎖混坞,通過緩存一致性協(xié)議來解決可見性問題。(可從Java內(nèi)存模型簡介了解緩存一致性協(xié)議)

0x01a3de1d:movb $0x0,0x1104800(%esi)  ; ...c6860048 100100
0x01a3de24:lock addl $0x0,(%esp)  ; ...f0830424 00

再比如下邊的例子

image.png

5.volatile如何保證有序性

  • 在分析保證有序性前钢坦,有必要了解一下內(nèi)存屏障究孕。內(nèi)存屏障(Memory Barriers,Intel稱之Memory Fence)指令是指爹凹,重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置蚊俺。CPU把內(nèi)存屏障分成三類:寫屏障(store barrier)、讀屏障(load barrier)和全屏障(Full Barrier)逛万。

  • 寫屏障store barrier相當(dāng)于storestore barrier, 強制所有在storestore內(nèi)存屏障之前的所有執(zhí)行,都要在該內(nèi)存屏障之前執(zhí)行,并發(fā)送緩存失效的信號宇植。所有在storestore barrier指令之后的store指令得封,都必須在storestore barrier屏障之前的指令執(zhí)行完后再被執(zhí)行。

讀屏障load barrier相當(dāng)于loadload barrier指郁,強制所有在load barrier讀屏障之后的load指令忙上,都在loadbarrier屏障之后執(zhí)行。

全屏障full barrier相當(dāng)于storeload闲坎,是一個全能型的屏障疫粥,因為它同時具備前面兩種屏障的效果。強制了所有在storeload barrier之前的store/load指令腰懂,都在該屏障之前被執(zhí)行梗逮,所有在該屏障之后的的store/load指令,都在該屏障之后被執(zhí)行绣溜。

在JMM中把內(nèi)存屏障指令分為4類:

  • LoadLoad Barriers慷彤,load1 ; LoadLoad; load2 ,確保load1數(shù)據(jù)的裝載優(yōu)先于load2及所有后續(xù)裝載指令的裝載怖喻。
  • StoreStore Barriers底哗,store1; storestore;store2 ,確保store1數(shù)據(jù)對其他處理器可見優(yōu)先于store2及所有后續(xù)存儲指令的存儲锚沸。
  • LoadStore Barries跋选, load1;loadstore;store2,確保load1數(shù)據(jù)裝載優(yōu)先于store2以及后續(xù)的存儲指令刷新到內(nèi)存哗蜈。
  • StoreLoad Barries前标, store1; storeload;load2, 確保store1數(shù)據(jù)對其他處理器變得可見恬叹, 優(yōu)先于load2及所有后續(xù)裝載指令的裝載候生;這條內(nèi)存屏障指令是一個全能型的屏障同時具有其他3條屏障的效果。

編譯器在生成字節(jié)碼時绽昼,會在volatile指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序唯鸭。對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能硅确。為此目溉,JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略菱农。

  • 在每個volatile寫操作的前面插入一個StoreStore屏障缭付。
  • 在每個volatile寫操作的后面插入一個StoreLoad屏障。
  • 在每個volatile讀操作的前面插入一個LoadLoad屏障循未。
  • 在每個volatile讀操作的后面插入一個LoadStore屏障陷猫。

6.volatile無法保證原子性

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。

把上面的代碼改成以下任何一種都可以達到效果:

(1)采用synchronized

public class Test {
    public  int inc = 0;
    
    public synchronized void increase() {
        inc++;
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

(2)采用Lock:

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

7.volatile的原理和實現(xiàn)機制

前面講述了源于volatile關(guān)鍵字的一些使用绣檬,下面我們來探討一下volatile到底如何保證可見性和禁止指令重排序的足陨。

“觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時娇未,會多出一個lock前綴指令”lock前綴指令實際上相當(dāng)于一個內(nèi)存屏障(也成內(nèi)存柵欄)墨缘,內(nèi)存屏障會提供3個功能:

  • 它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置,也不會把前面的指令排到內(nèi)存屏障的后面零抬;即在執(zhí)行到內(nèi)存屏障這句指令時镊讼,在它前面的操作已經(jīng)全部完成;
  • 它會強制將對緩存的修改操作立即寫入主存平夜;
  • 如果是寫操作蝶棋,它會導(dǎo)致其他CPU中對應(yīng)的緩存行無效。

8.volatile適用場景

  • synchronized關(guān)鍵字是防止多個線程同時執(zhí)行一段代碼褥芒,那么就會很影響程序執(zhí)行效率嚼松,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized,是一種比synchronized 關(guān)鍵字更輕量級的同步機制锰扶。但是要注意volatile關(guān)鍵字是無法替代synchronized關(guān)鍵字的献酗,因為volatile關(guān)鍵字無法保證操作的原子性。通常來說坷牛,使用volatile必須具備以下2個條件:

    • 對變量的寫操作不依賴于當(dāng)前值
    • 該變量沒有包含在具有其他變量的不變式中
  • 實際上罕偎,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立于任何程序的狀態(tài)京闰,包括變量的當(dāng)前狀態(tài)颜及。上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關(guān)鍵字的程序在并發(fā)時能夠正確執(zhí)行蹂楣。

9.思維導(dǎo)圖如下:

詳情介紹:

更新主題詳情

420天以來俏站,Java架構(gòu)更新了 888個主題,已經(jīng)有156+位同學(xué)加入痊土。微信掃碼關(guān)注java架構(gòu)肄扎,獲取Java面試題和架構(gòu)師相關(guān)題目和視頻。上述相關(guān)面試題答案赁酝,盡在Java架構(gòu)中犯祠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市酌呆,隨后出現(xiàn)的幾起案子衡载,更是在濱河造成了極大的恐慌,老刑警劉巖隙袁,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痰娱,死亡現(xiàn)場離奇詭異弃榨,居然都是意外死亡,警方通過查閱死者的電腦和手機猜揪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門惭墓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人而姐,你說我怎么就攤上這事』溃” “怎么了拴念?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長褐缠。 經(jīng)常有香客問我政鼠,道長,這世上最難降的妖魔是什么队魏? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任公般,我火速辦了婚禮,結(jié)果婚禮上胡桨,老公的妹妹穿的比我還像新娘官帘。我一直安慰自己,他們只是感情好昧谊,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布刽虹。 她就那樣靜靜地躺著,像睡著了一般呢诬。 火紅的嫁衣襯著肌膚如雪涌哲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天尚镰,我揣著相機與錄音阀圾,去河邊找鬼。 笑死狗唉,一個胖子當(dāng)著我的面吹牛初烘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播敞曹,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼账月,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了澳迫?” 一聲冷哼從身側(cè)響起局齿,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎橄登,沒想到半個月后抓歼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讥此,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年谣妻,在試婚紗的時候發(fā)現(xiàn)自己被綠了萄喳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蹋半,死狀恐怖他巨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情减江,我是刑警寧澤染突,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站辈灼,受9級特大地震影響份企,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜巡莹,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一司志、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧降宅,春花似錦骂远、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至唠雕,卻和暖如春贸营,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背岩睁。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工钞脂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捕儒。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓冰啃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親刘莹。 傳聞我的和親對象是個殘疾皇子阎毅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容