多線(xiàn)程知識(shí)梳理(8) - volatile 關(guān)鍵字

一、基本概念

1.1 內(nèi)存模型

在程序的執(zhí)行過(guò)程中,涉及到兩個(gè)方面:指令的執(zhí)行和數(shù)據(jù)的讀寫(xiě)萍肆。其中指令的執(zhí)行通過(guò)處理器來(lái)完成缠导,而數(shù)據(jù)的讀寫(xiě)則要依賴(lài)于系統(tǒng)內(nèi)存廉羔,但是處理器的執(zhí)行速度要遠(yuǎn)大于內(nèi)存數(shù)據(jù)的讀寫(xiě),因此在處理器中加入了高速緩存僻造。在程序的執(zhí)行過(guò)程中憋他,會(huì) 先將數(shù)據(jù)拷貝到處理器的高速緩存中,待運(yùn)算結(jié)束后再回寫(xiě)到系統(tǒng)內(nèi)存當(dāng)中髓削。

在單線(xiàn)程的情況下不會(huì)有什么問(wèn)題竹挡,但是如果在多線(xiàn)程情況下就可能會(huì)出現(xiàn)異常的情況,以下面這段代碼為例立膛,i是放在堆內(nèi)存的共享變量:

i = i + 1; //i 的初始值為0揪罕。

假如線(xiàn)程A和線(xiàn)程B都執(zhí)行這段代碼,那么就可能出現(xiàn)下面兩種情況:

  • 第一種情況:線(xiàn)程A先執(zhí)行+1操作宝泵,然后將i的值寫(xiě)回到系統(tǒng)內(nèi)存中好啰;線(xiàn)程B從系統(tǒng)內(nèi)存中拷貝i的值1到高速緩存中,執(zhí)行完+1操作再回寫(xiě)到系統(tǒng)內(nèi)存中儿奶,最終的結(jié)果是i=2框往。
  • 第二種情況:線(xiàn)程A和線(xiàn)程B首先都將i的值0拷貝到各自處理器的高速緩存當(dāng)中,線(xiàn)程A首先執(zhí)行+1操作闯捎,之后i的值為1椰弊,然后寫(xiě)回到系統(tǒng)內(nèi)存中;但是對(duì)于線(xiàn)程B而言瓤鼻,它并不知道這一過(guò)程秉版,在運(yùn)行該線(xiàn)程的處理器的高速緩存中i的值仍然為0,因此在它執(zhí)行+1操作后娱仔,再將i的值寫(xiě)回到系統(tǒng)內(nèi)存中沐飘,最終的結(jié)果是i=1

這種不確定性就稱(chēng)為 緩存不一致牲迫。

1.2 并發(fā)編程中的三個(gè)概念

在并發(fā)編程中耐朴,有三個(gè)關(guān)鍵的概念:可見(jiàn)性、原子性和有序性盹憎,只有保證了這三點(diǎn)才能使得程序在多線(xiàn)程情況下獲得預(yù)期的運(yùn)行結(jié)果筛峭。

1.2.1 可見(jiàn)性

可見(jiàn)性:是指線(xiàn)程之間的可見(jiàn)性,一個(gè)線(xiàn)程修改的狀態(tài)對(duì)另一個(gè)線(xiàn)程是可見(jiàn)的陪每。也就是一個(gè)線(xiàn)程修改的結(jié)果影晓,另一個(gè)線(xiàn)程馬上就能看到镰吵。在1.1所舉的例子就存在可見(jiàn)性的問(wèn)題。

Javavolatile挂签、synchronizedfinal實(shí)現(xiàn)可見(jiàn)性疤祭。

1.2.2 原子性

原子性:即一個(gè)操作或者多個(gè)操作,要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷饵婆,要么就都不執(zhí)行勺馆。

再比如a++,這個(gè)操作實(shí)際是a=a+1侨核,是可分割的草穆,所以它不是一個(gè)原子操作。非原子操作都會(huì)存在線(xiàn)程安全問(wèn)題搓译,需要我們使用同步技術(shù)來(lái)讓它變成一個(gè)原子操作悲柱。一個(gè)操作是原子操作,那么我們稱(chēng)它具有原子性些己。

Javasynchronized和在lock豌鸡、unlock中操作或者原子操作類(lèi)來(lái)保證原子性。

1.2.3 有序性

有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行轴总。以下面的代碼為例:

int i = 0;              
boolean flag = false;
i = 1; //語(yǔ)句1           
flag = true; //語(yǔ)句2

在上面的代碼中定義了一個(gè)整形和Boolean型變量直颅,并通過(guò)語(yǔ)句1和語(yǔ)句2對(duì)這兩個(gè)變量賦值博个,但是JVM在執(zhí)行這段代碼的時(shí)候并不保證語(yǔ)句1在語(yǔ)句2之前執(zhí)行怀樟,也就是說(shuō)可能會(huì)發(fā)生 指令重排序

指令重排序指的是在 保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致的前提 下盆佣,改變語(yǔ)句執(zhí)行的順序來(lái)優(yōu)化輸入代碼往堡,提高程序運(yùn)行效率。

但是這一前提條件在多線(xiàn)程的情況下就有可能出現(xiàn)問(wèn)題共耍,以下面的代碼為例:

//線(xiàn)程1:
context = loadContext(); //語(yǔ)句1
inited = true; //語(yǔ)句2
 
//線(xiàn)程2:
while (!inited) {
    sleep()
}
doSomethingWithConfig(context);

對(duì)于線(xiàn)程1來(lái)說(shuō)虑灰,語(yǔ)句1和語(yǔ)句2沒(méi)有依賴(lài)關(guān)系,因此有可能會(huì)發(fā)生指令重排序的情況痹兜。但是對(duì)于線(xiàn)程2來(lái)說(shuō)穆咐,語(yǔ)句2在語(yǔ)句1之前執(zhí)行,那么就會(huì)導(dǎo)致進(jìn)入doSomethingWithConfig函數(shù)的時(shí)候context沒(méi)有初始化字旭。

Java語(yǔ)言提供了volatilesynchronized兩個(gè)關(guān)鍵字來(lái)保證線(xiàn)程之間操作的有序性对湃,volatile是因?yàn)槠?本身包含禁止指令重排序 的語(yǔ)義,synchronized是由 一個(gè)變量在同一個(gè)時(shí)刻只允許一條線(xiàn)程對(duì)其進(jìn)行 lock 操作 這條規(guī)則獲得的遗淳,此規(guī)則決定了持有同一個(gè)對(duì)象鎖的兩個(gè)同步塊只能串行執(zhí)行拍柒。

二、volatile 詳解

2.1 定義

volatile的定義如下:Java編程語(yǔ)言允許線(xiàn)程訪(fǎng)問(wèn)共享變量屈暗,為了確保共享變量能被準(zhǔn)確和一致地更新拆讯,線(xiàn)程應(yīng)該確保 通過(guò)排它鎖單獨(dú)地獲得這個(gè)變量脂男。如果一個(gè)字段被聲明成volatileJava線(xiàn)程內(nèi)存模型確保 所有線(xiàn)程看到這個(gè)變量的值是一致的种呐。

一旦一個(gè)共享變量被volatile修飾之后宰翅,那么就具備了兩層語(yǔ)義:

  • 保證了不同線(xiàn)程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性,即一個(gè)線(xiàn)程修改了某個(gè)變量的值爽室,這新值對(duì)其他線(xiàn)程來(lái)說(shuō)是立即可見(jiàn)的堕油。
  • 禁止進(jìn)行指令重排序。

下面肮之,我們用兩個(gè)小結(jié)解釋一下這兩層語(yǔ)義掉缺。

2.2 保證可見(jiàn)性

當(dāng)我們?cè)?code>X86處理器下通過(guò)工具獲取JIT編譯器生成的匯編指令,來(lái)查看對(duì)volatile進(jìn)行寫(xiě)操作時(shí)戈擒,會(huì)發(fā)生下面的事情:

//Java 代碼
instance = new Singleton(); //instance 是 volatile 變量

//轉(zhuǎn)變成匯編代碼
0x01a3de1d: move $0 x 0, 0 x 1104800 (%esi); 
0x01a3de24: lock add1 $ 0 x 0, (%esp);

volatile變量修飾的共享變量 進(jìn)行寫(xiě)操作的時(shí)候 會(huì)多出兩行匯編代碼眶明,Lock前綴的指令在多核處理器下引發(fā)了兩件事情:

  • 將當(dāng)前處理器 內(nèi)部緩存 的數(shù)據(jù)寫(xiě)回到 系統(tǒng)內(nèi)存
  • 這個(gè)寫(xiě)回內(nèi)存的操作會(huì)使在其他處理器里 緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效筐高,當(dāng)這些處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候搜囱,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。

2.3 禁止指令重排序

volatile關(guān)鍵字禁止指令重排序有兩層意思:

  • 當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫(xiě)操作時(shí)柑土,在其前面的操作的更改肯定全部已經(jīng)進(jìn)行蜀肘,且結(jié)果已經(jīng)對(duì)后面的操作可見(jiàn);在其后面的操作肯定還沒(méi)有進(jìn)行稽屏;
  • 在進(jìn)行指令優(yōu)化時(shí)扮宠,不能將在對(duì)volatile變量訪(fǎng)問(wèn)的語(yǔ)句放在其后面執(zhí)行,也不能把volatile變量后面的語(yǔ)句放到其前面執(zhí)行狐榔。

以下面的例子為例:

//flag 為 volatile 變量

x = 2; //語(yǔ)句1
y = 0; //語(yǔ)句2
flag = true;  //語(yǔ)句3
x = 4; //語(yǔ)句4
y = -1; //語(yǔ)句5

由于flagvolatile變量坛增,因此,可以保證語(yǔ)句1/2在語(yǔ)句3之前執(zhí)行薄腻,語(yǔ)句4/5在其之后執(zhí)行收捣,但是并不保證語(yǔ)句1/2之間或者語(yǔ)句4/5之間的順序。

對(duì)于1.2.3舉的有關(guān)Context問(wèn)題庵楷,我們就可以通過(guò)將inited變量聲明為volatile罢艾,這樣就會(huì)保證loadContext()inited賦值語(yǔ)句之間的順序不被改變,避免出現(xiàn)inited=true但是Context沒(méi)有初始化的情況出現(xiàn)尽纽。

2.4 性能問(wèn)題

volatile相對(duì)于synchronized的優(yōu)勢(shì)主要原因是兩點(diǎn):簡(jiǎn)易和性能咐蚯。如果從讀寫(xiě)兩方便來(lái)考慮:

  • volatile讀操作開(kāi)銷(xiāo)非常低,幾乎和非volatile讀操作一樣
  • volatile寫(xiě)操作的開(kāi)銷(xiāo)要比非volatile寫(xiě)操作多很多蜓斧,因?yàn)橐WC可見(jiàn)性需要實(shí)現(xiàn) 內(nèi)存界定仓蛆,即便如此,volatile的總開(kāi)銷(xiāo)仍然要比鎖獲取低挎春。volatile操作不會(huì)像鎖一樣 造成阻塞看疙。

以上兩個(gè)條件表明豆拨,可以被寫(xiě)入volatile變量的這些有效值 獨(dú)立于任何程序的狀態(tài),包括變量的當(dāng)前狀態(tài)能庆。大多數(shù)的編程情形都會(huì)與這兩個(gè)條件的其中之一沖突施禾,使得volatile不能像synchronized那樣普遍適用于實(shí)現(xiàn)線(xiàn)程安全。

因此搁胆,在能夠安全使用volatile的情況下弥搞,volatile可以提供一些優(yōu)于鎖的可伸縮特性。如果讀操作的次數(shù)要遠(yuǎn)遠(yuǎn)超過(guò)寫(xiě)操作渠旁,與鎖相比攀例,volatile變量通常能夠減少同步的性能開(kāi)銷(xiāo)。

2.5 應(yīng)用場(chǎng)景

要使volatile變量提供理想的線(xiàn)程安全顾腊,必須同時(shí)滿(mǎn)足以下兩個(gè)條件:

  • 對(duì)變量的 寫(xiě)操作不依賴(lài)于當(dāng)前值粤铭。例如x++這樣的增量操作,它實(shí)際上是一個(gè)由讀取杂靶、修改梆惯、寫(xiě)入操作序列組成的組合操作,必須以原子方式執(zhí)行吗垮,而volatile不能提供必須的原子特性垛吗。
  • 該變量 沒(méi)有包含在其它變量的不變式中

避免濫用volatile最重要的準(zhǔn)則就是:只有在 狀態(tài)真正獨(dú)立于程序內(nèi)其它內(nèi)容時(shí) 才能使用volatile烁登,下面怯屉,我們總結(jié)一些volatile的應(yīng)用場(chǎng)景。

2.5.1 狀態(tài)標(biāo)志

volatile來(lái)修飾一個(gè)Boolean狀態(tài)標(biāo)志防泵,用于指示發(fā)生了某一次的重要事件蚀之,例如完成初始化或者請(qǐng)求停機(jī)蝗敢。

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

2.5.2 一次性安全發(fā)布

在解釋 一次性安全發(fā)布 的含義之前捷泞,讓我們先來(lái)看一下 單例寫(xiě)法 當(dāng)中著名的 雙重檢查鎖定問(wèn)題

    //使用 volatile 修飾寿谴。
    private volatile static Singleton sInstance;

    public static Singleton getInstance() {
        if (sInstance == null) { //(0)
            synchronized (Singleton.class) { //(1)         
                if (sInstance == null) {  //(2)           
                    sInstance = new Singleton(); //(3)    
                }
            }
        }
        return sInstance;
    }

假如 沒(méi)有使用volatile來(lái)修飾sInstance變量锁右,那么有可能會(huì)發(fā)生下面的場(chǎng)景:

  • 第一步:Thread1進(jìn)入getInstance()方法,由于sInstance為空讶泰,Thread1進(jìn)入synchronized代碼塊咏瑟。
  • 第二步:Thread1前進(jìn)到(3)處,在構(gòu)造函數(shù)執(zhí)行之前使sInstance對(duì)象成為非空痪署,并設(shè)置sInstance指向的內(nèi)存空間码泞。
  • 第三步:Thread2執(zhí)行,它在入口(0)處檢查實(shí)例是否為空狼犯,由于sInstance對(duì)象不為空余寥,Thread2sInstance引用返回领铐,此時(shí)sInstance對(duì)象并沒(méi)有初始化完成
  • 第四步:Thread1通過(guò)運(yùn)行Singleton對(duì)象的構(gòu)造函數(shù)并將引用返回給它宋舷,來(lái)完成對(duì)該對(duì)象的初始化绪撵。

通過(guò)volatile就可以禁止第二步和第四步的重排序,也就是使得 初始化對(duì)象在設(shè)置 sInstance 指向的內(nèi)存空間之前完成祝蝠。

2.5.3 volatile bean 模式

volatile bean模式適用于將JavaBeans作為“榮譽(yù)結(jié)構(gòu)”使用的框架音诈。在volatile bean模式中,JavaBean被用作一組具有getter和/或setter方法的獨(dú)立屬性的容器绎狭。

volatile bean模式的基本原理是:很多框架為易變數(shù)據(jù)的持有者提供了容器细溅,但是放入這些容器中的對(duì)象必須是線(xiàn)程安全的。

volatile bean模式中儡嘶,JavaBean的所有數(shù)據(jù)成員都是volatile類(lèi)型的谒兄,并且 gettersetter方法必須非常普通,除了獲取或設(shè)置相應(yīng)的屬性外社付,不能包含任何邏輯承疲。此外,對(duì)于對(duì)象引用的數(shù)據(jù)成員鸥咖,引用的對(duì)象必須是有效不可變的燕鸽。

public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;
 
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }
    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }
    public void setAge(int age) { 
        this.age = age;
    }
}

2.5.4 開(kāi)銷(xiāo)較低的讀/寫(xiě)鎖策略

如果讀操作遠(yuǎn)遠(yuǎn)超過(guò)寫(xiě)操作,您可以結(jié)合使用內(nèi)部鎖和volatile變量來(lái)減少公共代碼路徑的開(kāi)銷(xiāo)啼辣。下面的代碼中使用synchronized確保增量操作是原子的啊研,并使用volatile保證當(dāng)前結(jié)果的可見(jiàn)性。如果更新不頻繁的話(huà)鸥拧,該方法可實(shí)現(xiàn)更好的性能党远,因?yàn)樽x路徑的開(kāi)銷(xiāo)僅僅涉及volatile讀操作,這通常要優(yōu)于一個(gè)無(wú)競(jìng)爭(zhēng)的鎖獲取的開(kāi)銷(xiāo)富弦。

public class CheesyCounter {
    private volatile int value;
    public int getValue() { return value; }
    public synchronized int increment() {
        return value++;
    }
}

三沟娱、參考文獻(xiàn)

(1) Java 并發(fā)編程:volatile 關(guān)鍵字解析
(2) Java 中 volatile 關(guān)鍵字詳解
(3) 正確使用 volatile 變量
(4) volatile 的使用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市腕柜,隨后出現(xiàn)的幾起案子济似,更是在濱河造成了極大的恐慌,老刑警劉巖盏缤,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砰蠢,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡唉铜,警方通過(guò)查閱死者的電腦和手機(jī)台舱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)潭流,“玉大人竞惋,你說(shuō)我怎么就攤上這事俩功。” “怎么了碰声?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵诡蜓,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我胰挑,道長(zhǎng)蔓罚,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任瞻颂,我火速辦了婚禮豺谈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贡这。我一直安慰自己茬末,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布盖矫。 她就那樣靜靜地躺著宇驾,像睡著了一般兰迫。 火紅的嫁衣襯著肌膚如雪渔呵。 梳的紋絲不亂的頭發(fā)上馏段,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音湃望,去河邊找鬼换衬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛证芭,可吹牛的內(nèi)容都是我干的瞳浦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼废士,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼叫潦!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起湃密,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤诅挑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后泛源,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忿危,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年达箍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铺厨。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡缎玫,死狀恐怖硬纤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赃磨,我是刑警寧澤筝家,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站邻辉,受9級(jí)特大地震影響溪王,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜值骇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一莹菱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吱瘩,春花似錦道伟、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至票摇,卻和暖如春娜汁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背兄朋。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工掐禁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颅和。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓傅事,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親峡扩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蹭越,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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