volatile關(guān)鍵字的作用、原理

volatile關(guān)鍵字的作用外厂、原理

在只有雙重檢查鎖冕象,沒有volatile的懶加載單例模式中,由于指令重排序的問題汁蝶,我確實不會拿到兩個不同的單例了渐扮,但我會拿到“半個”單例

而發(fā)揮神奇作用的volatile掖棉,可以當之無愧的被稱為Java并發(fā)編程中“出現(xiàn)頻率最高的關(guān)鍵字”墓律,常用于保持內(nèi)存可見性和防止指令重排序。

保持內(nèi)存可見性

內(nèi)存可見性(Memory Visibility):所有線程都能看到共享內(nèi)存的最新狀態(tài)幔亥。

失效數(shù)據(jù)

以下是一個簡單的可變整數(shù)類:

public class MutableInteger {
private int value;
public int get(){
return value;
}
public void set(int value){
this.value = value;
}
}

MutableInteger不是線程安全的耻讽,因為getset方法都是在沒有同步的情況下進行的。如果線程1調(diào)用了set方法帕棉,那么正在調(diào)用的get的線程2可能會看到更新后的value值针肥,也可能看不到

解決方法很簡單香伴,將value聲明為volatile變量:

private volatile int value;

神奇的volatile關(guān)鍵字

神奇的volatile關(guān)鍵字解決了神奇的失效數(shù)據(jù)問題慰枕。

Java變量的讀寫

Java通過幾種原子操作完成工作內(nèi)存主內(nèi)存的交互:

  1. lock:作用于主內(nèi)存,把變量標識為線程獨占狀態(tài)即纲。
  2. unlock:作用于主內(nèi)存具帮,解除獨占狀態(tài)。
  3. read:作用主內(nèi)存低斋,把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存蜂厅。
  4. load:作用于工作內(nèi)存,把read操作傳過來的變量值放入工作內(nèi)存的變量副本中拔稳。
  5. use:作用工作內(nèi)存葛峻,把工作內(nèi)存當中的一個變量值傳給執(zhí)行引擎。
  6. assign:作用工作內(nèi)存巴比,把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量术奖。
  7. store:作用于工作內(nèi)存的變量,把工作內(nèi)存的一個變量的值傳送到主內(nèi)存中轻绞。
  8. write:作用于主內(nèi)存的變量采记,把store操作傳來的變量的值放入主內(nèi)存的變量中。

volatile如何保持內(nèi)存可見性

volatile的特殊規(guī)則就是:

  • read政勃、load唧龄、use動作必須連續(xù)出現(xiàn)
  • assign奸远、store既棺、write動作必須連續(xù)出現(xiàn)讽挟。

所以,使用volatile變量能夠保證:

  • 每次讀取前必須先從主內(nèi)存刷新最新的值丸冕。
  • 每次寫入后必須立即同步回主內(nèi)存當中耽梅。

也就是說,volatile關(guān)鍵字修飾的變量看到的隨時是自己的最新值胖烛。線程1中對變量v的最新修改眼姐,對線程2是可見的。

防止指令重排

在基于偏序關(guān)系Happens-Before內(nèi)存模型中佩番,指令重排技術(shù)大大提高了程序執(zhí)行效率众旗,但同時也引入了一些問題。

一個指令重排的問題——被部分初始化的對象

懶加載單例模式和競態(tài)條件

一個懶加載單例模式實現(xiàn)如下:

<figure class="highlight java" style="box-sizing: border-box; display: block; margin: 10px 0px; background: rgb(39, 40, 34); padding: 10px; overflow: auto; color: rgb(255, 255, 255); font-size: 0.9em; line-height: 22.4px; border-radius: 4px;">

|

class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if ( instance == null ) { //這里存在競態(tài)條件
instance = new Singleton();
}
return instance;
}
}

競態(tài)條件會導(dǎo)致instance引用被多次賦值趟畏,使用戶得到兩個不同的單例贡歧。

DCL和被部分初始化的對象

為了解決這個問題,可以使用synchronized關(guān)鍵字將getInstance方法改為同步方法拱镐;但這樣串行化的單例是不能忍的艘款。所以我猿族前輩設(shè)計了DCL(Double Check Lock持际,雙重檢查鎖)機制沃琅,使得大部分請求都不會進入阻塞代碼塊:

class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if ( instance == null ) { //當instance不為null時,仍可能指向一個“被部分初始化的對象”
synchronized (Singleton.class) {
if ( instance == null ) {
instance = new Singleton();
}
}
}
return instance;
}
}

“看起來”非常完美:既減少了阻塞蜘欲,又避免了競態(tài)條件益眉。不錯,但實際上仍然存在一個問題——當instance不為null時姥份,仍可能指向一個"被部分初始化的對象"郭脂。

問題出在這行簡單的賦值語句:

instance = new Singleton();

它并不是一個原子操作。事實上澈歉,它可以”抽象“為下面幾條JVM指令:

memory = allocate(); //1:分配對象的內(nèi)存空間
initInstance(memory); //2:初始化對象
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址

上面操作2依賴于操作1展鸡,但是操作3并不依賴于操作2,所以JVM可以以“優(yōu)化”為目的對它們進行重排序埃难,經(jīng)過重排序后如下:

memory = allocate(); //1:分配對象的內(nèi)存空間
instance = memory;//3:設(shè)置instance指向剛分配的內(nèi)存地址(此時對象還未初始化)
ctorInstance(memory); //2:初始化對象

可以看到指令重排之后莹弊,操作 3 排在了操作 2 之前,即引用instance指向內(nèi)存memory時涡尘,這段嶄新的內(nèi)存還沒有初始化——即忍弛,引用instance指向了一個”被部分初始化的對象”。此時考抄,如果另一個線程調(diào)用getInstance方法细疚,由于instance已經(jīng)指向了一塊內(nèi)存空間,從而if條件判為false川梅,方法返回instance引用疯兼,用戶得到了沒有完成初始化的“半個”單例然遏。

解決這個該問題,只需要將instance聲明為volatile變量:

private static volatile Singleton instance;

也就是說吧彪,在只有DCL沒有volatile的懶加載單例模式中啦鸣,仍然存在著并發(fā)陷阱。我確實不會拿到兩個不同的單例了来氧,但我會拿到“半個”單例(未完成初始化)诫给。
然而,許多面試書籍中啦扬,涉及懶加載的單例模式最多深入到DCL中狂,卻只字不提volatile。這“看似聰明”的機制扑毡,曾經(jīng)被我廣大初入Java世界的猿胞大加吹捧——我在大四實習(xí)面試跟誰學(xué)的時候胃榕,也得意洋洋的從飽漢、餓漢講到Double Check瞄摊,現(xiàn)在看來真是傻逼勋又。對于考查并發(fā)的面試官而言,單例模式的實現(xiàn)就是一個很好的切入點换帜,看似考查設(shè)計模式楔壤,其實期望你從設(shè)計模式答到并發(fā)和內(nèi)存模型。

volatile如何防止指令重排

volatile關(guān)鍵字通過“內(nèi)存屏障”來防止指令被重排序惯驼。

為了實現(xiàn)volatile的內(nèi)存語義蹲嚣,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序祟牲。然而隙畜,對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能说贝,為此议惰,Java內(nèi)存模型采取保守策略。

下面是基于保守策略的JMM內(nèi)存屏障插入策略:

  • 在每個volatile寫操作的前面插入一個StoreStore屏障乡恕。
  • 在每個volatile寫操作的后面插入一個StoreLoad屏障言询。
  • 在每個volatile讀操作的后面插入一個LoadLoad屏障。
  • 在每個volatile讀操作的后面插入一個LoadStore屏障几颜。

進階

在一次回答上述問題時倍试,忘記了解釋一個很容易引起疑惑的問題:

如果存在這種重排序問題,那么synchronized代碼塊內(nèi)部不是也可能出現(xiàn)相同的問題嗎蛋哭?

即這種情況:

class Singleton {
...
if ( instance == null ) { //可能發(fā)生不期望的指令重排
synchronized (Singleton.class) {
if ( instance == null ) {
instance = new Singleton();
System.out.println(instance.toString()); //程序順序規(guī)則發(fā)揮效力的地方
}
}
}
...
}

難道調(diào)用instance.toString()方法時县习,instance也可能未完成初始化嗎?

首先還請放寬心,synchronized代碼塊內(nèi)部雖然會重排序躁愿,但不會在代碼塊的范圍內(nèi)導(dǎo)致線程安全問題叛本。

Happens-Before內(nèi)存模型和程序順序規(guī)則

程序順序規(guī)則:如果程序中操作A在操作B之前,那么線程中操作A將在操作B之前執(zhí)行彤钟。

前面說過来候,只有在Happens-Before內(nèi)存模型中才會出現(xiàn)這樣的指令重排序問題。Happens-Before內(nèi)存模型維護了幾種Happens-Before規(guī)則逸雹,程序順序規(guī)則最基本的規(guī)則营搅。程序順序規(guī)則的目標對象是一段程序代碼中的兩個操作A、B梆砸,其保證此處的指令重排不會破壞操作A转质、B在代碼中的先后順序,但與不同代碼甚至不同線程中的順序無關(guān)帖世。

因此休蟹,在synchronized代碼塊內(nèi)部,instance = new Singleton()仍然會指令重排序日矫,但重排序之后的所有指令赂弓,仍然能夠保證在instance.toString()之前執(zhí)行。進一步的哪轿,單線程中盈魁,if ( instance == null )能保證在synchronized代碼塊之前執(zhí)行;但多線程中缔逛,線程1中的if ( instance == null )卻與線程2中的synchronized代碼塊之間沒有偏序關(guān)系备埃,因此線程2中synchronized代碼塊內(nèi)部的指令重排對于線程1是不期望的姓惑,導(dǎo)致了此處的并發(fā)陷阱褐奴。

類似的Happens-Before規(guī)則還有volatile變量規(guī)則監(jiān)視器鎖規(guī)則等于毙。程序猿可以借助(Piggyback)現(xiàn)有的Happens-Before規(guī)則來保持內(nèi)存可見性和防止指令重排敦冬。

注意點

上面簡單講解了volatile關(guān)鍵字的作用和原理,但對volatile的使用過程中很容易出現(xiàn)的一個問題是:

錯把volatile變量當做原子變量唯沮。

出現(xiàn)這種誤解的原因脖旱,主要是volatile關(guān)鍵字使變量的讀、寫具有了“原子性”介蛉。然而這種原子性僅限于變量(包括引用)的讀和寫萌庆,無法涵蓋變量上的任何操作,即:

  • 基本類型的自增(如count++)等操作不是原子的币旧。
  • 對象的任何非原子成員調(diào)用(包括成員變量成員方法)不是原子的践险。

如果希望上述操作也具有原子性,那么只能采取鎖、原子變量更多的措施巍虫。

總結(jié)

綜上彭则,其實volatile保持內(nèi)存可見性和防止指令重排序的原理,本質(zhì)上是同一個問題占遥,也都依靠內(nèi)存屏障得到解決俯抖。更多內(nèi)容請參見JVM相關(guān)書籍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瓦胎,一起剝皮案震驚了整個濱河市芬萍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌搔啊,老刑警劉巖担忧,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異坯癣,居然都是意外死亡瓶盛,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門示罗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惩猫,“玉大人,你說我怎么就攤上這事蚜点≡浚” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵绍绘,是天一觀的道長奶镶。 經(jīng)常有香客問我,道長陪拘,這世上最難降的妖魔是什么厂镇? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮左刽,結(jié)果婚禮上捺信,老公的妹妹穿的比我還像新娘。我一直安慰自己欠痴,他們只是感情好迄靠,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著喇辽,像睡著了一般掌挚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上菩咨,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天吠式,我揣著相機與錄音舅世,去河邊找鬼。 笑死奇徒,一個胖子當著我的面吹牛雏亚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播摩钙,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼罢低,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胖笛?” 一聲冷哼從身側(cè)響起网持,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎长踊,沒想到半個月后功舀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡身弊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年辟汰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阱佛。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡帖汞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出凑术,到底是詐尸還是另有隱情翩蘸,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布淮逊,位于F島的核電站催首,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏泄鹏。R本人自食惡果不足惜郎任,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望命满。 院中可真熱鬧涝滴,春花似錦、人聲如沸胶台。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诈唬。三九已至,卻和暖如春缩麸,著一層夾襖步出監(jiān)牢的瞬間铸磅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留阅仔,地道東北人吹散。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像八酒,于是被迫代替她去往敵國和親空民。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

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

  • 在只有雙重檢查鎖,沒有volatile的懶加載單例模式中衔瓮,由于指令重排序的問題浊猾,我確實不會拿到兩個不同的單例了,但...
    猴子007閱讀 921評論 0 6
  • 從三月份找實習(xí)到現(xiàn)在热鞍,面了一些公司葫慎,掛了不少,但最終還是拿到小米薇宠、百度幅疼、阿里、京東昼接、新浪爽篷、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,213評論 11 349
  • volatile關(guān)鍵字經(jīng)常在并發(fā)編程中使用,其特性是保證可見性以及有序性漂辐,但是關(guān)于volatile的使用仍然要小心...
    Ruheng閱讀 10,043評論 40 135
  • (一) 十二圈泪喊,不多不少。熊小姐輕輕抽出銀質(zhì)的小勺髓涯,慢條斯理地擱在了骨瓷的杯盤中袒啼,“叮”的一聲有如人間仙樂纬纪。 咖啡...
    第一千零一只太陽閱讀 408評論 0 0
  • 大家晚上好蚓再。我是思楠。周三的晚上包各,我來了摘仅。和平常一樣倦踢,分享自己有關(guān)于職業(yè)規(guī)劃的東西 謝謝大家的回應(yīng)鸯乃。今晚我分享的主...
    思楠生涯規(guī)劃閱讀 183評論 0 0