并發(fā)編程之volatile

寫在前面

前面一章我們講了了java原子性的相關(guān)概念和知識點(diǎn),介紹了用于共享變量線程隔離的ThreadLocal分扎,也知道了synchronized是一個重量級的鎖笼蛛,而我們今天要講的volatile則是輕量級的synchronized奴艾,主要是因?yàn)樗粫鹁€程上下文的切換橄妆。在講volatile到底是什么,它能夠解決什么樣的問題之前符匾,首先不得不提一下Java的內(nèi)存模型叨咖。

JMM內(nèi)存模型

在Java中,所有實(shí)例域待讳,靜態(tài)域和數(shù)組對象都存在于堆中芒澜,是所有線程共享的。局部變量创淡,方法參數(shù)和異常處理參數(shù)不是線程共享痴晦,不會存在內(nèi)存可見性問題。

Java線程之間的通信由Java(簡稱JMM)內(nèi)存模型來控制琳彩,它決定了對一個變量的寫入何時對另一個線程可見誊酌。JMM規(guī)定了所有的共享變量都存在于主內(nèi)存中,而每條線程又有自己的工作內(nèi)存露乏,工作內(nèi)存內(nèi)保存了對于主內(nèi)存中共享變量的拷貝碧浊。我們對一個變量的讀寫是在工作內(nèi)存中完成的,同時線程間的通信是由工作內(nèi)存將修改的變量刷新到主內(nèi)存來進(jìn)行傳遞的瘟仿。下面是JMM的抽象示意圖:

01.png

從圖上看箱锐,線程A和B進(jìn)行通信的話分成兩步:

  • 線程A將修改后的共享變量刷新到主內(nèi)存
  • 線程B從主內(nèi)存讀取線程A已更新過的共享變量

由于線程B每次都是從主內(nèi)存拿變量,并不能實(shí)時地獲取線程A修改的變量值劳较,可能讀取的是之前的值驹止,從而出現(xiàn)臟讀浩聋,這就是不滿足可見性的問題。

可見性問題

可見性是指當(dāng)多個線程訪問同一個共享變量時臊恋,一個線程修改了這個變量的值衣洁,另一個線程立馬能夠讀取到修改后的值。

舉個栗子:

//線程1
int i = 0;
i = 10;
//線程2
j=i;

畫個圖:

02.png

由圖可知抖仅,假如線程A,B按照這種時間順序執(zhí)行的話坊夫,j最后的值是0。這就是可見性問題撤卢,線程A對變量i修改的值环凿,沒有立即對線程B可見。

有序性問題

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

舉個栗子:

public class VolatileDemo {
    private static boolean flag;
    private static int num;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
                while (!flag) {
                    Thread.yield();
                }
                System.out.println(num);
        });
        t1.start();
        num = 5;
        flag = true;
    }
}

這段代碼拷邢,盡管num=5是寫在flag=true前面,但是最終打印的結(jié)果有可能是0哦屎慢。也就是說,寫在前面的代碼并沒有先執(zhí)行忽洛,對于這種不按書寫順序執(zhí)行的情況稱作指令重排序腻惠。大多數(shù)現(xiàn)代處理器都支持指令重排,為的是直接運(yùn)行當(dāng)前能運(yùn)行的指令欲虚,而不去順序等待集灌,這種亂序的執(zhí)行方式打打提高了處理器的效率。

戲說不是胡說复哆,改編不是亂編欣喧,指令重排也不是隨便排,它是根據(jù)代碼的依賴關(guān)系梯找,在不影響單線程環(huán)境下的執(zhí)行結(jié)果的前提下進(jìn)行重排序的唆阿。例如:

a=1;
b=2;
c=a+b;

這段代碼,c=a+b是不會重排到啊a,b之前的锈锤,因?yàn)閏的值對a,b都有依賴驯鳖。但是,a,b的賦值語句可能會重排久免。這種指令重排在單線程環(huán)境中沒有任何問題浅辙,但是在多線程的環(huán)境下,就將會出現(xiàn)數(shù)據(jù)的不確定性阎姥。

volatile保證可見性和有序性记舆,但不能保證原子性

在Java中,大佬們給我們提供了volatile關(guān)鍵字來保證可見性和有序性呼巴。這個說法有兩種語義:

  • 保證了不同線程對變量操作時的可見性泽腮,即一個線程修改了變量的值御蒲,對另一個線程是立馬可見的摔桦。

  • 禁止指令重排序稍味,即在volatile語句之前的代碼不會重排序到volatile語句之后去執(zhí)行。

    volatile保證了可見性奶赔,是因?yàn)槭褂胿olatile關(guān)鍵字會強(qiáng)制把值寫入主內(nèi)存豪筝,同時將其他線程工作內(nèi)存中的緩存行無效痰滋,這樣其他線程在獲取變量值的時候,發(fā)現(xiàn)了自己的緩存行無效后续崖,在對應(yīng)的主內(nèi)存地址被更新后就會去主內(nèi)存中取敲街。

    volatile保證了有序性,如果兩個操作的執(zhí)行次序無法從happens-befor原則推導(dǎo)出來严望,那么它們就不能保證它們的有序性多艇,虛擬機(jī)可以隨意地對它們進(jìn)行重排序。volatile底層是通過內(nèi)存屏障來完成一系列的有序性功能的像吻。

    注:synchronizedlock也能保證有序性峻黍。

最后要強(qiáng)調(diào)一點(diǎn)的是,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);
    }
}

這段代碼最終的輸出結(jié)果將小于10000姆涩,原因就在于inc++它不是個原子性操作,盡管volatile能保證對inc的修改立即被其他線程感知惭每,但是對于inc的并發(fā)讀取不會觸發(fā)強(qiáng)制刷新主內(nèi)存骨饿,也不會導(dǎo)致其他線程的緩存行無效,這樣就導(dǎo)致多個線程讀取到同樣的值台腥。

一般這種情況可以用synchronizedlock來解決宏赘,也可以通過無鎖CAS方式的AtomicInteger來解決。

所以說黎侈,重量級鎖還是比較穩(wěn)的察署,volatile不能完全替代synchronized,使用volatile必須具備兩個條件:

  • 對變量的寫入不依賴當(dāng)前值
  • 該變量沒有包含在其他變量的不變式中

參考資料

  1. 方騰飛:《Java并發(fā)編程的藝術(shù)》
  2. 指令重排序
  3. 深入分析volatile的實(shí)現(xiàn)原理
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜓竹,一起剝皮案震驚了整個濱河市箕母,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌俱济,老刑警劉巖嘶是,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蛛碌,居然都是意外死亡聂喇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來希太,“玉大人克饶,你說我怎么就攤上這事√芑裕” “怎么了矾湃?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長堕澄。 經(jīng)常有香客問我邀跃,道長,這世上最難降的妖魔是什么蛙紫? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任拍屑,我火速辦了婚禮,結(jié)果婚禮上坑傅,老公的妹妹穿的比我還像新娘僵驰。我一直安慰自己,他們只是感情好唁毒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布蒜茴。 她就那樣靜靜地躺著,像睡著了一般浆西。 火紅的嫁衣襯著肌膚如雪矮男。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天室谚,我揣著相機(jī)與錄音,去河邊找鬼崔泵。 笑死秒赤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的憎瘸。 我是一名探鬼主播入篮,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼幌甘!你這毒婦竟也來了潮售?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤锅风,失蹤者是張志新(化名)和其女友劉穎酥诽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皱埠,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肮帐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片训枢。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡托修,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出恒界,到底是詐尸還是另有隱情睦刃,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布十酣,位于F島的核電站涩拙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏婆誓。R本人自食惡果不足惜吃环,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望洋幻。 院中可真熱鬧郁轻,春花似錦、人聲如沸文留。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽燥翅。三九已至骑篙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間森书,已是汗流浹背靶端。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凛膏,地道東北人杨名。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像猖毫,于是被迫代替她去往敵國和親台谍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

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