volatile關(guān)鍵字的原理及適用場景(摘選)

一铡买、volatile的作用

《Java并發(fā)編程:核心理論》一文中,我們已經(jīng)提到過可見性、有序性及原子性問題极谊,通常情況下我們可以通過Synchronized關(guān)鍵字來解決這些個問題,不過如果對Synchronized原理有了解的話安岂,應(yīng)該知道Synchronized是一個比較重量級的操作轻猖,對系統(tǒng)的性能有比較大的影響,所以域那,如果有其他解決方案咙边,我們通常都避免使用Synchronized來解決問題。而volatile關(guān)鍵字就是Java中提供的另一種解決可見性和有序性問題的方案次员。對于原子性败许,需要強(qiáng)調(diào)一點(diǎn),也是大家容易誤解的一點(diǎn):對volatile變量的單次讀/寫操作可以保證原子性的淑蔚,如long和double類型變量市殷,但是并不能保證i++這種操作的原子性,因為本質(zhì)上i++是讀刹衫、寫兩次操作醋寝。

二搞挣、volatile的使用

關(guān)于volatile的使用,我們可以通過幾個例子來說明其使用方式和場景音羞。

1囱桨、防止重排序

我們從一個最經(jīng)典的例子來分析重排序問題。大家應(yīng)該都很熟悉單例模式的實(shí)現(xiàn)嗅绰,而在并發(fā)環(huán)境下的單例實(shí)現(xiàn)方式舍肠,我們通常可以采用雙重檢查加鎖(DCL)的方式來實(shí)現(xiàn)办陷。其源碼如下:

package com.paddx.test.concurrent;

public class Singleton {

public static volatile Singleton singleton;

/**

* 構(gòu)造函數(shù)私有貌夕,禁止外部實(shí)例化

*/

private Singleton() {};

public static Singleton getInstance() {

if (singleton == null) {

? ? synchronized (singleton) {

? ? ? ? if (singleton == null) {

? ? ? ? ? ? ? ? singleton = new Singleton();

? ? ? ? ? ? }

? ? ? ? }

? }

? ? return singleton;

? ? }

}

現(xiàn)在我們分析一下為什么要在變量singleton之間加上volatile關(guān)鍵字。要理解這個問題民镜,先要了解對象的構(gòu)造過程啡专,實(shí)例化一個對象其實(shí)可以分為三個步驟:

(1)分配內(nèi)存空間。

(2)初始化對象制圈。

(3)將內(nèi)存空間的地址賦值給對應(yīng)的引用们童。

但是由于操作系統(tǒng)可以對指令進(jìn)行重排序,所以上面的過程也可能會變成如下過程:

(1)分配內(nèi)存空間鲸鹦。

(2)將內(nèi)存空間的地址賦值給對應(yīng)的引用慧库。

(3)初始化對象

如果是這個流程,多線程環(huán)境下就可能將一個未初始化的對象引用暴露出來馋嗜,從而導(dǎo)致不可預(yù)料的結(jié)果齐板。因此,為了防止這個過程的重排序葛菇,我們需要將變量設(shè)置為volatile類型的變量甘磨。

2、實(shí)現(xiàn)可見性

可見性問題主要指一個線程修改了共享變量值眯停,而另一個線程卻看不到济舆。引起可見性問題的主要原因是每個線程擁有自己的一個高速緩存區(qū)——線程工作內(nèi)存。volatile關(guān)鍵字能有效的解決這個問題莺债,我們看下下面的例子滋觉,就可以知道其作用:

package com.paddx.test.concurrent;

public class VolatileTest {

int a = 1;

int b = 2;

public void change(){

a = 3;

b = a;

}

public void print(){

System.out.println("b="+b+";a="+a);

}

public static void main(String[] args) {

while (true){

final VolatileTest test = new VolatileTest();

new Thread(new Runnable() {

@Override

public void run() {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

test.change();

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

test.print();

}

}).start();

}

}

}

直觀上說,這段代碼的結(jié)果只可能有兩種:b=3;a=3 或 b=2;a=1齐邦。不過運(yùn)行上面的代碼(可能時間上要長一點(diǎn))椎侠,你會發(fā)現(xiàn)除了上兩種結(jié)果之外,還出現(xiàn)了第三種結(jié)果:

......

b=2;a=1

b=2;a=1

b=3;a=3

b=3;a=3

b=3;a=1

b=3;a=3

b=2;a=1

b=3;a=3

b=3;a=3

......

為什么會出現(xiàn)b=3;a=1這種結(jié)果呢措拇?正常情況下我纪,如果先執(zhí)行change方法,再執(zhí)行print方法,輸出結(jié)果應(yīng)該為b=3;a=3宣羊。相反璧诵,如果先執(zhí)行的print方法,再執(zhí)行change方法仇冯,結(jié)果應(yīng)該是 b=2;a=1之宿。那b=3;a=1的結(jié)果是怎么出來的?原因就是第一個線程將值a=3修改后苛坚,但是對第二個線程是不可見的比被,所以才出現(xiàn)這一結(jié)果。如果將a和b都改成volatile類型的變量再執(zhí)行泼舱,則再也不會出現(xiàn)b=3;a=1的結(jié)果了等缀。

3、保證原子性

關(guān)于原子性的問題娇昙,上面已經(jīng)解釋過尺迂。volatile只能保證對單次讀/寫的原子性。這個問題可以看下JLS中的描述:

17.7 Non-Atomic Treatment of double and long

For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.

Writes and reads of volatile long and double values are always atomic.

Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.

Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.

Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.

這段話的內(nèi)容跟我前面的描述內(nèi)容大致類似冒掌。因為long和double兩種數(shù)據(jù)類型的操作可分為高32位和低32位兩部分噪裕,因此普通的long或double類型讀/寫可能不是原子的。因此股毫,鼓勵大家將共享的long和double變量設(shè)置為volatile類型膳音,這樣能保證任何情況下對long和double的單次讀/寫操作都具有原子性。

關(guān)于volatile變量對原子性保證铃诬,有一個問題容易被誤解〖老荩現(xiàn)在我們就通過下列程序來演示一下這個問題:

package com.paddx.test.concurrent;

public class VolatileTest01 {

volatile int i;

public void addI(){

i++;

}

public static void main(String[] args) throws InterruptedException {

final? VolatileTest01 test01 = new VolatileTest01();

for (int n = 0; n < 1000; n++) {

new Thread(new Runnable() {

@Override

public void run() {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

test01.addI();

}

}).start();

}

Thread.sleep(10000);//等待10秒,保證上面程序執(zhí)行完成

System.out.println(test01.i);

}

}

大家可能會誤認(rèn)為對變量i加上關(guān)鍵字volatile后趣席,這段程序就是線程安全的兵志。大家可以嘗試運(yùn)行上面的程序。下面是我本地運(yùn)行的結(jié)果:

可能每個人運(yùn)行的結(jié)果不相同吩坝。不過應(yīng)該能看出毒姨,volatile是無法保證原子性的(否則結(jié)果應(yīng)該是1000)哑蔫。原因也很簡單钉寝,i++其實(shí)是一個復(fù)合操作,包括三步驟:

(1)讀取i的值闸迷。

(2)對i加1嵌纲。

(3)將i的值寫回內(nèi)存。

volatile是無法保證這三個操作是具有原子性的腥沽,我們可以通過AtomicInteger或者Synchronized來保證+1操作的原子性逮走。

注:上面幾段代碼中多處執(zhí)行了Thread.sleep()方法,目的是為了增加并發(fā)問題的產(chǎn)生幾率今阳,無其他作用师溅。

三茅信、volatile的原理

通過上面的例子,我們基本應(yīng)該知道了volatile是什么以及怎么使用∧钩簦現(xiàn)在我們再來看看volatile的底層是怎么實(shí)現(xiàn)的蘸鲸。

1、可見性實(shí)現(xiàn):

在前文中已經(jīng)提及過窿锉,線程本身并不直接與主內(nèi)存進(jìn)行數(shù)據(jù)的交互酌摇,而是通過線程的工作內(nèi)存來完成相應(yīng)的操作。這也是導(dǎo)致線程間數(shù)據(jù)不可見的本質(zhì)原因嗡载。因此要實(shí)現(xiàn)volatile變量的可見性窑多,直接從這方面入手即可。對volatile變量的寫操作與普通變量的主要區(qū)別有兩點(diǎn):

(1)修改volatile變量時會強(qiáng)制將修改后的值刷新的主內(nèi)存中洼滚。

(2)修改volatile變量后會導(dǎo)致其他線程工作內(nèi)存中對應(yīng)的變量值失效埂息。因此,再讀取該變量值的時候就需要重新從讀取主內(nèi)存中的值遥巴。

通過這兩個操作耿芹,就可以解決volatile變量的可見性問題。

2挪哄、有序性實(shí)現(xiàn):

在解釋這個問題前吧秕,我們先來了解一下Java中的happen-before規(guī)則,JSR 133中對Happen-before的定義如下:

Two actions can be ordered by a happens-before relationship.If one action happens before another, then the first is visible to and ordered before the second.

通俗一點(diǎn)說就是如果a happen-before b迹炼,則a所做的任何操作對b是可見的砸彬。(這一點(diǎn)大家務(wù)必記住,因為happen-before這個詞容易被誤解為是時間的前后)斯入。我們再來看看JSR 133中定義了哪些happen-before規(guī)則:

? Each action in a thread happens before every subsequent action in that thread.

? An unlock on a monitor happens before every subsequent lock on that monitor.

? A write to a volatile field happens before every subsequent read of that volatile.

? A call to start() on a thread happens before any actions in the started thread.

? All actions in a thread happen before any other thread successfully returns from a join() on that thread.

? If an action a happens before an action b, and b happens before an action c, then a happens before c.

翻譯過來為:

同一個線程中的砂碉,前面的操作 happen-before 后續(xù)的操作。(即單線程內(nèi)按代碼順序執(zhí)行刻两。但是增蹭,在不影響在單線程環(huán)境執(zhí)行結(jié)果的前提下,編譯器和處理器可以進(jìn)行重排序磅摹,這是合法的滋迈。換句話說,這一是規(guī)則無法保證編譯重排和指令重排)户誓。

監(jiān)視器上的解鎖操作 happen-before 其后續(xù)的加鎖操作饼灿。(Synchronized 規(guī)則)

對volatile變量的寫操作 happen-before 后續(xù)的讀操作。(volatile 規(guī)則)

線程的start() 方法 happen-before 該線程所有的后續(xù)操作帝美。(線程啟動規(guī)則)

線程所有的操作 happen-before 其他線程在該線程上調(diào)用 join 返回成功后的操作碍彭。

如果 a happen-before b,b happen-before c,則a happen-before c(傳遞性)庇忌。

這里我們主要看下第三條:volatile變量的保證有序性的規(guī)則舞箍。《Java并發(fā)編程:核心理論》一文中提到過重排序分為編譯器重排序和處理器重排序。為了實(shí)現(xiàn)volatile內(nèi)存語義皆疹,JMM會對volatile變量限制這兩種類型的重排序创译。下面是JMM針對volatile變量所規(guī)定的重排序規(guī)則表:

Can Reorder2nd operation

1st operationNormal Load

Normal Store

Volatile LoadVolatile Store

Normal Load

Normal Store

No

Volatile LoadNoNoNo

Volatile storeNoNo

3、內(nèi)存屏障

為了實(shí)現(xiàn)volatile可見性和happen-befor的語義墙基。JVM底層是通過一個叫做“內(nèi)存屏障”的東西來完成软族。內(nèi)存屏障,也叫做內(nèi)存柵欄残制,是一組處理器指令立砸,用于實(shí)現(xiàn)對內(nèi)存操作的順序限制。下面是完成上述規(guī)則所要求的內(nèi)存屏障:

Required barriers2nd operation

1st operationNormal LoadNormal StoreVolatile LoadVolatile Store

Normal LoadLoadStore

Normal StoreStoreStore

Volatile LoadLoadLoadLoadStoreLoadLoadLoadStore

Volatile StoreStoreLoadStoreStore

(1)LoadLoad 屏障

執(zhí)行順序:Load1—>Loadload—>Load2

確保Load2及后續(xù)Load指令加載數(shù)據(jù)之前能訪問到Load1加載的數(shù)據(jù)初茶。

(2)StoreStore 屏障

執(zhí)行順序:Store1—>StoreStore—>Store2

確保Store2以及后續(xù)Store指令執(zhí)行前颗祝,Store1操作的數(shù)據(jù)對其它處理器可見。

(3)LoadStore 屏障

執(zhí)行順序: Load1—>LoadStore—>Store2

確保Store2和后續(xù)Store指令執(zhí)行前恼布,可以訪問到Load1加載的數(shù)據(jù)螺戳。

(4)StoreLoad 屏障

執(zhí)行順序: Store1—> StoreLoad—>Load2

確保Load2和后續(xù)的Load指令讀取之前,Store1的數(shù)據(jù)對其他處理器是可見的折汞。

最后我可以通過一個實(shí)例來說明一下JVM中是如何插入內(nèi)存屏障的:

package com.paddx.test.concurrent;

public class MemoryBarrier {

int a, b;

volatile int v, u;

void f() {

int i, j;

i = a;

j = b;

i = v;

//LoadLoad

j = u;

//LoadStore

a = i;

b = j;

//StoreStore

v = i;

//StoreStore

u = j;

//StoreLoad

i = u;

//LoadLoad

//LoadStore

j = b;

a = i;

}

}

四倔幼、總結(jié)

總體上來說volatile的理解還是比較困難的,如果不是特別理解爽待,也不用急损同,完全理解需要一個過程,在后續(xù)的文章中也還會多次看到volatile的使用場景鸟款。這里暫且對volatile的基礎(chǔ)知識和原來有一個基本的了解膏燃。總體來說何什,volatile是并發(fā)編程中的一種優(yōu)化组哩,在某些場景下可以代替Synchronized。但是处渣,volatile的不能完全取代Synchronized的位置伶贰,只有在一些特殊的場景下,才能適用volatile霍比∧桓ぃ總的來說暴备,必須同時滿足下面兩個條件才能保證在并發(fā)環(huán)境的線程安全:

(1)對變量的寫操作不依賴于當(dāng)前值悠瞬。

(2)該變量沒有包含在具有其他變量的不變式中。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浅妆,隨后出現(xiàn)的幾起案子望迎,更是在濱河造成了極大的恐慌,老刑警劉巖凌外,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辩尊,死亡現(xiàn)場離奇詭異,居然都是意外死亡康辑,警方通過查閱死者的電腦和手機(jī)摄欲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疮薇,“玉大人胸墙,你說我怎么就攤上這事“粗洌” “怎么了迟隅?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長励七。 經(jīng)常有香客問我智袭,道長,這世上最難降的妖魔是什么掠抬? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任吼野,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蚯嫌。我一直安慰自己泞歉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布谚攒。 她就那樣靜靜地躺著,像睡著了一般氛堕。 火紅的嫁衣襯著肌膚如雪馏臭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天讼稚,我揣著相機(jī)與錄音括儒,去河邊找鬼。 笑死锐想,一個胖子當(dāng)著我的面吹牛帮寻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赠摇,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼固逗,長吁一口氣:“原來是場噩夢啊……” “哼浅蚪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起烫罩,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤惜傲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贝攒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盗誊,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年隘弊,在試婚紗的時候發(fā)現(xiàn)自己被綠了哈踱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡梨熙,死狀恐怖嚣鄙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情串结,我是刑警寧澤哑子,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站肌割,受9級特大地震影響卧蜓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜把敞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一弥奸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奋早,春花似錦盛霎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至掉奄,卻和暖如春规个,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背姓建。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工诞仓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人速兔。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓墅拭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親涣狗。 傳聞我的和親對象是個殘疾皇子谍婉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

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

  • 瑾安知何暖目錄 慕暖看著近在咫尺的冷酷俊臉舒憾,心里特想罵娘,靠屡萤!你是誰啊珍剑,本姑娘怕你干嘛掸宛?本姑娘只是不想跟冰山在一起...
    暖丫頭閱讀 444評論 6 2
  • 組別 301期 利他一組 【日精進(jìn)打卡第108天】 【知~學(xué)習(xí)】 誦讀《六項精進(jìn)大綱》3遍死陆,累計225遍;誦讀《...
    J0hn先生閱讀 108評論 0 0
  • 夜爬峨嵋唧瘾,十二小時六十公里海拔三千米措译,選了最難的一條線路。九十九道拐名不虛傳饰序。 想要的不過是一段經(jīng)歷领虹,不過是在平淡...
    檸檬草微青cc閱讀 206評論 0 0
  • 幾個現(xiàn)實(shí)生活中看似觸手可及的人,靈魂卻是陌生的求豫,然而在同一個方面找到了相似之處塌衰。于是所有人的人生好像被分割成了三個...
    C056閱讀 1,075評論 0 0
  • css組織命名方式(CSS架構(gòu))主要有OOCSS,BEM,SMACSS,MVCSS等。關(guān)于css framewor...
    thisiswoa閱讀 902評論 0 1