原文鏈接: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 修飾之后,那么就具備了兩層語義:
- 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性妄讯,即一個(gè)線程修改了某個(gè)變量的值孩锡,這新值對(duì)其他線程來說是立即可見的
- 禁止進(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)系可以分為兩類:
- 根據(jù)程序次序規(guī)則蝶缀,1 happens before 2; 3 happens before 4。
- 根據(jù) volatile 規(guī)則薄货,2 happens before 3翁都。
- 根據(jù) happens before 的傳遞性規(guī)則,1 happens before 4谅猾。
上述 happens before 關(guān)系的圖形化表現(xiàn)形式如下:
在上圖中柄慰,每一個(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)示意圖:
如上圖所示乐设,線程 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)示意圖:
如上圖所示舶沛,在讀 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)存屏障后生成的指令序列示意圖:
上圖中的 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)存屏障后生成的指令序列示意圖:
上圖中的 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)化:
注意,最后的 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)化成:
前文提到過穆律,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í)行:
在舊的內(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;
}
}