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

原文鏈接:https://bestzuo.cn/posts/4234991750.html

在 JSR-133 (也即 JDK1.5 )之前逊谋,volatile 關(guān)鍵字一直飽受爭(zhēng)議调榄,因?yàn)槭褂眠@個(gè)關(guān)鍵字會(huì)造成一些不可預(yù)料的后果,從 JSR-133 開始,專家組對(duì)這個(gè)關(guān)鍵字的語義進(jìn)行了增強(qiáng)喇肋,從而使得現(xiàn)在使用 volatile 關(guān)鍵字的環(huán)境越來越多鸣戴,目前它也被稱為是“輕量級(jí)的 synchronized”。

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

volatile 雖然從字面意思上理解比較簡(jiǎn)單昧廷,但是在實(shí)際環(huán)境中能正確的使用該變量并不容易堪嫂,只有了解其背后的原理,我們才能發(fā)揮出這個(gè)關(guān)鍵字的重要作用木柬。在理解原理之前皆串,我們有必要先了解一下并發(fā)編程中三個(gè)常見的概念:原子性、可見性和順序一致性弄诲。

原子性

原子性理解比較簡(jiǎn)單愚战,與數(shù)據(jù)庫(kù)系統(tǒng)中原子性意思基本一致,即一個(gè)操作或者多個(gè)操作要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷齐遵,要么就都不執(zhí)行寂玲。

一個(gè)很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題:

比如從賬戶 A 向賬戶 B 轉(zhuǎn) 1000 元,那么必然包括 2 個(gè)操作:從賬戶 A 減去 1000 元梗摇,往賬戶 B 加上 1000 元拓哟。

試想一下,如果這 2 個(gè)操作不具備原子性伶授,會(huì)造成什么樣的后果断序。假如從賬戶 A 減去 1000 元之后癞季,操作突然中止辛燥。然后又從 B 取出了 500 元隐解,取出 500 元之后白修,再執(zhí)行往賬戶 B 加上 1000 元的操作奈应。這樣就會(huì)導(dǎo)致賬戶 A 雖然減去了 1000 元录平,但是賬戶 B 沒有收到這個(gè)轉(zhuǎn)過來的 1000 元塔鳍。

所以這 2 個(gè)操作必須要具備原子性才能保證不出現(xiàn)一些意外的問題汉矿。

同樣地反映到并發(fā)編程中會(huì)出現(xiàn)什么結(jié)果呢?

舉個(gè)最簡(jiǎn)單的例子阵苇,大家想一下假如為一個(gè) 64 位的變量賦值過程不具備原子性的話壁公,會(huì)發(fā)生什么后果?

long i = 9L;

假若一個(gè)線程執(zhí)行到這個(gè)語句時(shí)绅项,我暫且假設(shè)為一個(gè) 64 位的變量賦值包括兩個(gè)過程:為低 32 位賦值紊册,為高 32 位賦值。那么就可能發(fā)生一種情況:當(dāng)將低 32 位數(shù)值寫入之后快耿,突然被中斷囊陡,而此時(shí)又有一個(gè)線程去讀取 i 的值,那么讀取到的就是錯(cuò)誤的數(shù)據(jù)润努。

可見性

可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí)关斜,一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值铺浇。即一個(gè)線程修改的值能對(duì)其它線程可見痢畜。

可見性的原理涉及到 Java 內(nèi)存模型,關(guān)于其中的 Java 內(nèi)存模型可以參考我的另外一篇博客鳍侣,以便于下文的理解丁稀。

有了 JMM 的概念后,可以舉個(gè)簡(jiǎn)單的例子倚聚,看下面這段代碼:

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

假若執(zhí)行線程 1 的是 CPU1 线衫,執(zhí)行線程 2 的是 CPU2。由上面的分析可知惑折,當(dāng)線程 1 執(zhí)行 i = 10 這句時(shí)授账,會(huì)先把 i 的初始值加載到 CPU1 的高速緩存中,然后賦值為 10惨驶,那么在 CPU1 的高速緩存當(dāng)中 i 的值變?yōu)?10 了白热,卻沒有立即寫入到主存當(dāng)中。

此時(shí)線程 2 執(zhí)行 j = i粗卜,它會(huì)先去主存讀取i的值并加載到 CPU2 的緩存當(dāng)中屋确,注意此時(shí)內(nèi)存當(dāng)中 i 的值還是 0,那么就會(huì)使得 j 的值為 0续扔,而不是 10攻臀。

這就是可見性問題,線程 1 對(duì)變量 i 修改了之后纱昧,線程 2 沒有立即看到線程 1 修改的值刨啸。

順序一致性

即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。舉個(gè)簡(jiǎn)單的例子识脆,看下面這段代碼:

int i = 0;              
boolean flag = false;
i = 1;                //語句1  
flag = true;          //語句2

上面代碼定義了一個(gè) int 型變量设联,定義了一個(gè) boolean 類型變量加匈,然后分別對(duì)兩個(gè)變量進(jìn)行賦值操作。從代碼順序上看仑荐,語句 1 是在語句 2 前面的,那么 JVM 在真正執(zhí)行這段代碼的時(shí)候會(huì)保證語句 1 一定會(huì)在語句 2 前面執(zhí)行嗎纵东?不一定粘招,為什么呢?這里可能會(huì)發(fā)生指令重排序(Instruction Reorder)偎球。

下面解釋一下什么是指令重排序洒扎,一般來說,處理器為了提高程序運(yùn)行效率衰絮,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化袍冷,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的猫牡。

比如上面的代碼中胡诗,語句 1 和語句 2 誰先執(zhí)行對(duì)最終的程序結(jié)果并沒有影響,那么就有可能在執(zhí)行過程中淌友,語句 2 先執(zhí)行而語句 1 后執(zhí)行煌恢。

但是要注意,雖然處理器會(huì)對(duì)指令進(jìn)行重排序震庭,但是它會(huì)保證程序最終結(jié)果會(huì)和代碼順序執(zhí)行結(jié)果相同瑰抵。關(guān)于指令重排序的具體細(xì)節(jié)可以參考我的另外一篇文章

volatile的定義

有了并發(fā)編程的三個(gè)基本概念后器联,我們就可以看一下 volatile 相關(guān)的定義了二汛。這里引用 JSR-133 中對(duì) volatile 關(guān)鍵字的定義:

The Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.

A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).

簡(jiǎn)單翻譯一下:

Java編程語言中允許線程訪問共享變量。為了確保共享變量能被一致地和可靠的更新拨拓,線程必須確保它是排他性的使用此共享變量肴颊,通常都是獲得對(duì)這些共享變量強(qiáng)制排他性的同步鎖。

Java編程語言提供了另一種機(jī)制千元,volatile 域變量苫昌,對(duì)于某些場(chǎng)景的使用這要更加的方便。

可以把變量聲明為 volatile幸海,以讓 Java 內(nèi)存模型來保證所有線程都能看到這個(gè)變量的同一個(gè)值祟身。

簡(jiǎn)而言之,volatile 相當(dāng)于提供了一種同步機(jī)制物独,從而保證被 volatile 關(guān)鍵字聲明的變量對(duì)所有線程的可見性袜硫,并且 volatile 不會(huì)引起線程上下文的切換和調(diào)度,比 synchronized 的執(zhí)行成本要更低挡篓。

volatile內(nèi)存語義

有了 volatile 的定義后婉陷,我們比較疑惑的地方就是帚称, volatile 提供了一種什么樣的“同步機(jī)制”,是如何保證變量對(duì)所有線程的可見性的秽澳。只要理解了這兩個(gè)問題的原理闯睹,volatile 關(guān)鍵字就沒有那么可怕了。

實(shí)際上担神, 一旦一個(gè)共享變量(類的成員變量楼吃、類的靜態(tài)成員變量)被 volatile 修飾之后,那么就具備了兩層語義:

  1. 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性妄讯,即一個(gè)線程修改了某個(gè)變量的值孩锡,這新值對(duì)其他線程來說是立即可見的
  2. 禁止進(jìn)行指令重排序

對(duì)上面兩層語義,我們逐步進(jìn)行解析亥贸。

volatile 的特性

當(dāng)我們聲明共享變量為 volatile 后躬窜,對(duì)這個(gè)變量的讀 / 寫將會(huì)很特別。理解 volatile 特性的一個(gè)好方法是:把對(duì) volatile 變量的單個(gè)讀 / 寫炕置,看成是使用同一個(gè)監(jiān)視器鎖對(duì)這些單個(gè)讀 / 寫操作做了同步荣挨。下面我們通過具體的示例來說明,請(qǐng)看下面的示例代碼:

class VolatileFeaturesExample {
    volatile long vl = 0L;  // 使用 volatile 聲明 64 位的 long 型變量 

    public void set(long l) {
        vl = l;   // 單個(gè) volatile 變量的寫 
    }

    public void getAndIncrement () {
        vl++;    // 復(fù)合(多個(gè))volatile 變量的讀 / 寫 
    }

    public long get() {
        return vl;   // 單個(gè) volatile 變量的讀 
    }
}

假設(shè)有多個(gè)線程分別調(diào)用上面程序的三個(gè)方法讹俊,這個(gè)程序在語意上和下面程序等價(jià):

class VolatileFeaturesExample {
    long vl = 0L;               // 64 位的 long 型普通變量 

    public synchronized void set(long l) {     // 對(duì)單個(gè)的普通 變量的寫用同一個(gè)監(jiān)視器同步 
        vl = l;
    }

    public void getAndIncrement () { // 普通方法調(diào)用 
        long temp = get();           // 調(diào)用已同步的讀方法 
        temp += 1L;                  // 普通寫操作 
        set(temp);                   // 調(diào)用已同步的寫方法 
    }
    public synchronized long get() { 
    // 對(duì)單個(gè)的普通變量的讀用同一個(gè)監(jiān)視器同步 
        return vl;
    }
}

如上面示例程序所示垦沉,對(duì)一個(gè) volatile 變量的單個(gè)讀 / 寫操作,與對(duì)一個(gè)普通變量的讀 / 寫操作使用同一個(gè)監(jiān)視器鎖來同步仍劈,它們之間的執(zhí)行效果相同厕倍。

監(jiān)視器鎖的 happens-before 規(guī)則保證釋放監(jiān)視器和獲取監(jiān)視器的兩個(gè)線程之間的內(nèi)存可見性,這意味著對(duì)一個(gè) volatile 變量的讀贩疙,總是能看到(任意線程)對(duì)這個(gè) volatile 變量最后的寫入讹弯。

監(jiān)視器鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性。這意味著即使是 64 位的 long 型和 double 型變量这溅,只要它是 volatile 變量组民,對(duì)該變量的讀寫就將具有原子性。如果是多個(gè) volatile 操作或類似于 volatile++ 這種復(fù)合操作悲靴,這些操作整體上不具有原子性臭胜。

簡(jiǎn)而言之,volatile 變量自身具有下列特性:

  • 可見性:對(duì)一個(gè) volatile 變量的讀癞尚,總是能看到(任意線程)對(duì)這個(gè) volatile 變量最后的寫入耸三。
  • 原子性:對(duì)任意單個(gè) volatile 變量的讀 / 寫具有原子性,但類似于 volatile++ 這種復(fù)合操作不具有原子性浇揩。

volatile 寫 - 讀建立的 happens before 關(guān)系

上面講的是 volatile 變量自身的特性仪壮,對(duì)程序員來說,volatile 對(duì)線程的內(nèi)存可見性的影響比 volatile 自身的特性更為重要胳徽,也更需要我們?nèi)リP(guān)注积锅。

從 JSR-133 開始爽彤,volatile 變量的寫 - 讀可以實(shí)現(xiàn)線程之間的通信。

從內(nèi)存語義的角度來說缚陷,volatile 與監(jiān)視器鎖有相同的效果:volatile 寫和監(jiān)視器的釋放有相同的內(nèi)存語義适篙;volatile 讀與監(jiān)視器的獲取有相同的內(nèi)存語義。

請(qǐng)看下面使用 volatile 變量的示例代碼:

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;             //2
    }

    public void reader() {
        if (flag) {               //3
            int i =  a;           //4
            ……
        }
    }
}

假設(shè)線程 A 執(zhí)行 writer() 方法之后箫爷,線程 B 執(zhí)行 reader() 方法匙瘪。根據(jù) happens before 規(guī)則,這個(gè)過程建立的 happens before 關(guān)系可以分為兩類:

  1. 根據(jù)程序次序規(guī)則蝶缀,1 happens before 2; 3 happens before 4。
  2. 根據(jù) volatile 規(guī)則薄货,2 happens before 3翁都。
  3. 根據(jù) happens before 的傳遞性規(guī)則,1 happens before 4谅猾。

上述 happens before 關(guān)系的圖形化表現(xiàn)形式如下:

image

在上圖中柄慰,每一個(gè)箭頭鏈接的兩個(gè)節(jié)點(diǎn),代表了一個(gè) happens before 關(guān)系税娜。黑色箭頭表示程序順序規(guī)則坐搔;橙色箭頭表示 volatile 規(guī)則;藍(lán)色箭頭表示組合這些規(guī)則后提供的 happens before 保證敬矩。

這里 A 線程寫一個(gè) volatile 變量后概行,B 線程讀同一個(gè) volatile 變量。A 線程在寫 volatile 變量之前所有可見的共享變量弧岳,在 B 線程讀同一個(gè) volatile 變量后凳忙,將立即變得對(duì) B 線程可見。

volatile 寫 - 讀的內(nèi)存語義

volatile 寫的內(nèi)存語義如下:

當(dāng)寫一個(gè) volatile 變量時(shí)禽炬,JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存涧卵。

以上面示例程序 VolatileExample 為例,假設(shè)線程 A 首先執(zhí)行 writer() 方法腹尖,隨后線程 B 執(zhí)行 reader() 方法柳恐,初始時(shí)兩個(gè)線程的本地內(nèi)存中的 flag 和 a 都是初始狀態(tài)。下圖是線程 A 執(zhí)行 volatile 寫后热幔,共享變量的狀態(tài)示意圖:

image

如上圖所示乐设,線程 A 在寫 flag 變量后,本地內(nèi)存 A 中被線程 A 更新過的兩個(gè)共享變量的值被刷新到主內(nèi)存中断凶。此時(shí)伤提,本地內(nèi)存 A 和主內(nèi)存中的共享變量的值是一致的。

volatile 讀的內(nèi)存語義如下:

當(dāng)讀一個(gè) volatile 變量時(shí)认烁,JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效肿男。線程接下來將從主內(nèi)存中讀取共享變量介汹。

下面是線程 B 讀同一個(gè) volatile 變量后,共享變量的狀態(tài)示意圖:

image

如上圖所示舶沛,在讀 flag 變量后嘹承,本地內(nèi)存 B 已經(jīng)被置為無效。此時(shí)如庭,線程 B 必須從主內(nèi)存中讀取共享變量叹卷。線程 B 的讀取操作將導(dǎo)致本地內(nèi)存 B 與主內(nèi)存中的共享變量的值也變成一致的了。

如果我們把 volatile 寫和 volatile 讀這兩個(gè)步驟綜合起來看的話坪它,在讀線程 B 讀一個(gè) volatile 變量后骤竹,寫線程 A 在寫這個(gè) volatile 變量之前所有可見的共享變量的值都將立即變得對(duì)讀線程 B 可見。

下面對(duì) volatile 寫和 volatile 讀的內(nèi)存語義做個(gè)總結(jié)

  • 線程 A 寫一個(gè) volatile 變量往毡,實(shí)質(zhì)上是線程 A 向接下來將要讀這個(gè) volatile 變量的某個(gè)線程發(fā)出了(其對(duì)共享變量所在修改的)消息蒙揣。
  • 線程 B 讀一個(gè) volatile 變量,實(shí)質(zhì)上是線程 B 接收了之前某個(gè)線程發(fā)出的(在寫這個(gè) volatile 變量之前對(duì)共享變量所做修改的)消息开瞭。
  • 線程 A 寫一個(gè) volatile 變量懒震,隨后線程 B 讀這個(gè) volatile 變量,這個(gè)過程實(shí)質(zhì)上是線程 A 通過主內(nèi)存向線程 B 發(fā)送消息嗤详。

volatile 內(nèi)存語義的實(shí)現(xiàn)

下面个扰,讓我們來看看 JMM 如何實(shí)現(xiàn) volatile 寫 / 讀的內(nèi)存語義。

前文我們提到過重排序分為編譯器重排序和處理器重排序葱色。為了實(shí)現(xiàn) volatile 內(nèi)存語義递宅,JMM 會(huì)分別限制這兩種類型的重排序類型。下面是 JMM 針對(duì)編譯器制定的 volatile 重排序規(guī)則表:

是否能重排序 第二個(gè)操作
第一個(gè)操作 普通讀 / 寫 volatile 讀 volatile 寫
普通讀 / 寫 NO
volatile 讀 NO NO NO
volatile 寫 NO NO

舉例來說苍狰,第三行最后一個(gè)單元格的意思是:在程序順序中恐锣,當(dāng)?shù)谝粋€(gè)操作為普通變量的讀或?qū)憰r(shí),如果第二個(gè)操作為 volatile 寫舞痰,則編譯器不能重排序這兩個(gè)操作土榴。

從上表我們可以看出:

  • 當(dāng)?shù)诙€(gè)操作是 volatile 寫時(shí),不管第一個(gè)操作是什么响牛,都不能重排序玷禽。這個(gè)規(guī)則確保 volatile 寫之前的操作不會(huì)被編譯器重排序到 volatile 寫之后。
  • 當(dāng)?shù)谝粋€(gè)操作是 volatile 讀時(shí)呀打,不管第二個(gè)操作是什么矢赁,都不能重排序。這個(gè)規(guī)則確保 volatile 讀之后的操作不會(huì)被編譯器重排序到 volatile 讀之前贬丛。
  • 當(dāng)?shù)谝粋€(gè)操作是 volatile 寫撩银,第二個(gè)操作是 volatile 讀時(shí),不能重排序豺憔。

為了實(shí)現(xiàn) volatile 的內(nèi)存語義额获,編譯器在生成字節(jié)碼時(shí)够庙,會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。對(duì)于編譯器來說抄邀,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能耘眨,為此,JMM 采取保守策略境肾。下面是基于保守策略的 JMM 內(nèi)存屏障插入策略:

  • 在每個(gè) volatile 寫操作的前面插入一個(gè) StoreStore 屏障剔难。
  • 在每個(gè) volatile 寫操作的后面插入一個(gè) StoreLoad 屏障。
  • 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadLoad 屏障奥喻。
  • 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadStore 屏障偶宫。

上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺(tái)环鲤,任意的程序中都能得到正確的 volatile 內(nèi)存語義读宙。

下面是保守策略下,volatile 寫插入內(nèi)存屏障后生成的指令序列示意圖:

image

上圖中的 StoreStore 屏障可以保證在 volatile 寫之前楔绞,其前面的所有普通寫操作已經(jīng)對(duì)任意處理器可見了。這是因?yàn)?StoreStore 屏障將保障上面所有的普通寫在 volatile 寫之前刷新到主內(nèi)存唇兑。

這里比較有意思的是 volatile 寫后面的 StoreLoad 屏障酒朵。這個(gè)屏障的作用是避免 volatile 寫與后面可能有的 volatile 讀 / 寫操作重排序。因?yàn)榫幾g器常常無法準(zhǔn)確判斷在一個(gè) volatile 寫的后面扎附,是否需要插入一個(gè) StoreLoad 屏障(比如蔫耽,一個(gè) volatile 寫之后方法立即 return)。為了保證能正確實(shí)現(xiàn) volatile 的內(nèi)存語義留夜,JMM 在這里采取了保守策略:在每個(gè) volatile 寫的后面或在每個(gè) volatile 讀的前面插入一個(gè) StoreLoad 屏障匙铡。從整體執(zhí)行效率的角度考慮,JMM 選擇了在每個(gè) volatile 寫的后面插入一個(gè) StoreLoad 屏障碍粥。因?yàn)?volatile 寫 - 讀內(nèi)存語義的常見使用模式是:一個(gè)寫線程寫 volatile 變量鳖眼,多個(gè)讀線程讀同一個(gè) volatile 變量。當(dāng)讀線程的數(shù)量大大超過寫線程時(shí)嚼摩,選擇在 volatile 寫之后插入 StoreLoad 屏障將帶來可觀的執(zhí)行效率的提升钦讳。從這里我們可以看到 JMM 在實(shí)現(xiàn)上的一個(gè)特點(diǎn):首先確保正確性,然后再去追求執(zhí)行效率枕面。

下面是在保守策略下愿卒,volatile 讀插入內(nèi)存屏障后生成的指令序列示意圖:

image

上圖中的 LoadLoad 屏障用來禁止處理器把上面的 volatile 讀與下面的普通讀重排序。LoadStore 屏障用來禁止處理器把上面的 volatile 讀與下面的普通寫重排序潮秘。

上述 volatile 寫和 volatile 讀的內(nèi)存屏障插入策略非常保守琼开。在實(shí)際執(zhí)行時(shí),只要不改變 volatile 寫 - 讀的內(nèi)存語義枕荞,編譯器可以根據(jù)具體情況省略不必要的屏障柜候。下面我們通過具體的示例代碼來說明:

class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           // 第一個(gè) volatile 讀 
        int j = v2;           // 第二個(gè) volatile 讀 
        a = i + j;            // 普通寫 
        v1 = i + 1;          // 第一個(gè) volatile 寫 
        v2 = j * 2;          // 第二個(gè) volatile 寫 
    }

    …                    // 其他方法 
}

針對(duì) readAndWrite() 方法搞动,編譯器在生成字節(jié)碼時(shí)可以做如下的優(yōu)化:

image

注意,最后的 StoreLoad 屏障不能省略改橘。因?yàn)榈诙€(gè) volatile 寫之后滋尉,方法立即 return。此時(shí)編譯器可能無法準(zhǔn)確斷定后面是否會(huì)有 volatile 讀或?qū)懛芍鳎瑸榱税踩鹨娛ㄏВ幾g器常常會(huì)在這里插入一個(gè) StoreLoad 屏障。

上面的優(yōu)化是針對(duì)任意處理器平臺(tái)碌识,由于不同的處理器有不同“松緊度”的處理器內(nèi)存模型碾篡,內(nèi)存屏障的插入還可以根據(jù)具體的處理器內(nèi)存模型繼續(xù)優(yōu)化。以 x86 處理器為例筏餐,上圖中除最后的 StoreLoad 屏障外开泽,其它的屏障都會(huì)被省略。

前面保守策略下的 volatile 讀和寫魁瞪,在 x86 處理器平臺(tái)可以優(yōu)化成:

image

前文提到過穆律,x86 處理器僅會(huì)對(duì)寫 - 讀操作做重排序。X86 不會(huì)對(duì)讀 - 讀导俘,讀 - 寫寫 - 寫操作做重排序峦耘,因此在 x86 處理器中會(huì)省略掉這三種操作類型對(duì)應(yīng)的內(nèi)存屏障。在 x86 中旅薄,JMM 僅需在 volatile 寫后面插入一個(gè) StoreLoad 屏障即可正確實(shí)現(xiàn) volatile 寫 - 讀的內(nèi)存語義辅髓。這意味著在 x86 處理器中,volatile 寫的開銷比 volatile 讀的開銷會(huì)大很多(因?yàn)閳?zhí)行 StoreLoad 屏障開銷會(huì)比較大)少梁。

JSR-133 為什么要增強(qiáng) volatile 的內(nèi)存語義

在 JSR-133 之前的舊 Java 內(nèi)存模型中洛口,雖然不允許 volatile 變量之間重排序,但舊的 Java 內(nèi)存模型允許 volatile 變量與普通變量之間重排序凯沪。在舊的內(nèi)存模型中第焰,VolatileExample 示例程序可能被重排序成下列時(shí)序來執(zhí)行:

image

在舊的內(nèi)存模型中,當(dāng) 1 和 2 之間沒有數(shù)據(jù)依賴關(guān)系時(shí)妨马,1 和 2 之間就可能被重排序(3 和 4 類似)樟遣。其結(jié)果就是:讀線程 B 執(zhí)行 4 時(shí),不一定能看到寫線程 A 在執(zhí)行 1 時(shí)對(duì)共享變量的修改身笤。

因此在舊的內(nèi)存模型中 豹悬,volatile 的寫 - 讀沒有監(jiān)視器的釋放 - 獲所具有的內(nèi)存語義。為了提供一種比監(jiān)視器鎖更輕量級(jí)的線程之間通信的機(jī)制液荸,JSR-133 專家組決定增強(qiáng) volatile 的內(nèi)存語義:嚴(yán)格限制編譯器和處理器對(duì) volatile 變量與普通變量的重排序瞻佛,確保 volatile 的寫 - 讀和監(jiān)視器的釋放 - 獲取一樣,具有相同的內(nèi)存語義。從編譯器重排序規(guī)則和處理器內(nèi)存屏障插入策略來看伤柄,只要 volatile 變量與普通變量之間的重排序可能會(huì)破壞 volatile 的內(nèi)存語意绊困,這種重排序就會(huì)被編譯器重排序規(guī)則和處理器內(nèi)存屏障插入策略禁止。

由于 volatile 僅僅保證對(duì)單個(gè) volatile 變量的讀 / 寫具有原子性适刀,而監(jiān)視器鎖的互斥執(zhí)行的特性可以確保對(duì)整個(gè)臨界區(qū)代碼的執(zhí)行具有原子性秤朗。在功能上,監(jiān)視器鎖比 volatile 更強(qiáng)大笔喉;在可伸縮性和執(zhí)行性能上取视,volatile 更有優(yōu)勢(shì)。如果想在程序中用 volatile 代替監(jiān)視器鎖常挚,請(qǐng)一定謹(jǐn)慎作谭。

volatile的使用場(chǎng)景

在了解了 volatile 關(guān)鍵字的原理之后,我們可以做一個(gè)小結(jié)奄毡,volatile 關(guān)鍵字可以為一個(gè)變量提供一種同步訪問機(jī)制折欠,如果一個(gè)變量被聲明為 volatile ,那么編譯器和 JVM 就知道該變量是可能被另一個(gè)線程并發(fā)更新的吼过。

<div class="note info">那么 volatile 關(guān)鍵字能提供這種機(jī)制的原理就是锐秦,如果寫入 volatile 聲明的變量,JMM 會(huì)通過強(qiáng)制將本地內(nèi)存刷新到主內(nèi)存中盗忱,如果讀取 volatile 聲明的變量酱床, JMM 會(huì)將本地內(nèi)存置為無效,同時(shí)強(qiáng)制從主內(nèi)存中讀取該變量數(shù)據(jù)到本地內(nèi)存中售淡,從而通過這個(gè)機(jī)制保證 volatile 變量的可見性。另一方面慷垮,volatile 變量在讀/寫時(shí)會(huì)在每個(gè)操作前后插入一個(gè)內(nèi)存屏障揖闸,從而禁止編譯器和處理器指令重排序以保證其語義。</div>

那么可以引申出 volatile 變量的使用場(chǎng)景料身,要使 volatile 變量提供理想的線程安全汤纸,必須同時(shí)滿足下面兩個(gè)條件:

  • 對(duì)變量的寫操作不依賴于當(dāng)前值。
  • 該變量沒有包含在具有其他變量的不變式中芹血。

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

使用 volatile 聲明的布爾類型變量贮泞,可以在一些情景中達(dá)到很好的效果,比如如下從一個(gè)線程終止另外一個(gè)線程幔烛。

反例:

private static boolean stopThread;
public static void main(String[] args) throws InterruptedException {
   Thread th = new Thread(new Runnable() {
      @Override
      public void run() {
         int i = 0;
         while (!stopThread) {
            i++;
         }
      }
   });
   th.start();
   TimeUnit.SECONDS.sleep(2);
   stopThread = true;
}

運(yùn)行后發(fā)現(xiàn)該程序根本無法終止循環(huán)啃擦,原因是,Java 語言規(guī)范并不保證一個(gè)線程寫入的值對(duì)另外一個(gè)線程是可見的饿悬,所以即使主線程 main 函數(shù)修改了共享變量 stopThread 狀態(tài)令蛉,但是對(duì) th 線程并不一定可見,最終導(dǎo)致循環(huán)無法終止。

正例:

private static volatile boolean stopThread;
public static void main(String[] args) throws InterruptedException {
   Thread th = new Thread(new Runnable() {
      @Override
      public void run() {
         int i = 0;
         while (!stopThread) {
            i++;
         }
      }
   });
   th.start();
   TimeUnit.SECONDS.sleep(2);
   stopThread = true;
}

通過使用關(guān)鍵字 volatile 修飾共享變量 stopThread珠叔,根據(jù) volatile 的可見性原則可以保證主線程 main 函數(shù)修改了共享變量 stopThread 狀態(tài)后對(duì)線程 th 來說是立即可見的蝎宇,所以在兩秒內(nèi)線程 th 將停止循環(huán)。

雙重檢查鎖

可以作為單例模式的一種實(shí)現(xiàn)方法祷安,具體為什么要這么實(shí)現(xiàn)可以參考我的另外一篇博客姥芥。

public class Singleton {
    private volatile static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();
                }
            }
        }
        return uniqueSingleton;
    }
}

參考文章

  1. 方騰飛等 著 《Java 并發(fā)編程的藝術(shù)》
  2. Java理論與實(shí)踐:正確使用 Volatile 變量
  3. 聊聊并發(fā)(一)深入分析Volatile的實(shí)現(xiàn)原理
  4. Java并發(fā)編程:volatile關(guān)鍵字解析
  5. Java關(guān)鍵字volatile的理解與正確使用
  6. JSR-133: JavaTM Memory Model and Thread Specification
  7. The JSR-133 Cookbook for Compiler Writers
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市汇鞭,隨后出現(xiàn)的幾起案子凉唐,更是在濱河造成了極大的恐慌,老刑警劉巖虱咧,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熊榛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡腕巡,警方通過查閱死者的電腦和手機(jī)玄坦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绘沉,“玉大人煎楣,你說我怎么就攤上這事〕瞪。” “怎么了择懂?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)另玖。 經(jīng)常有香客問我困曙,道長(zhǎng),這世上最難降的妖魔是什么谦去? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任慷丽,我火速辦了婚禮,結(jié)果婚禮上鳄哭,老公的妹妹穿的比我還像新娘要糊。我一直安慰自己,他們只是感情好妆丘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布锄俄。 她就那樣靜靜地躺著,像睡著了一般勺拣。 火紅的嫁衣襯著肌膚如雪奶赠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天药有,我揣著相機(jī)與錄音车柠,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛竹祷,可吹牛的內(nèi)容都是我干的谈跛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼塑陵,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼感憾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起令花,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤阻桅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后兼都,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嫂沉,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年扮碧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了趟章。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡慎王,死狀恐怖蚓土,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赖淤,我是刑警寧澤蜀漆,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站咱旱,受9級(jí)特大地震影響确丢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吐限,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一鲜侥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧毯盈,春花似錦剃毒、人聲如沸病袄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽益缠。三九已至脑奠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間幅慌,已是汗流浹背宋欺。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人齿诞。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓酸休,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親祷杈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子斑司,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

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