并發(fā)編程03-Java內(nèi)存模型03(雙重檢查鎖定與延遲初始化)

在Java多線程程序當(dāng)中,有時(shí)候需要采用延遲初始化來降低初始化類和創(chuàng)建對(duì)象的開銷.雙檢查鎖定是常見的延遲初始化結(jié)束,但他是一個(gè)錯(cuò)誤的用法.

雙重檢查鎖定的由來

在Java程序中,有時(shí)候可能需要推遲一些高開銷的對(duì)象初始化操作先誉,并且只有在使用這些對(duì)象時(shí)才進(jìn)行初始化野建。此時(shí)贪绘,程序員可能會(huì)采用延遲初始化悄窃。但要正確實(shí)現(xiàn)線程安全的延遲初始化需要一些技巧嚎杨,否則很容易出現(xiàn)問題蹬耘。


在UnsafeLazyInitialization類中造烁,假設(shè)A線程執(zhí)行代碼1的同時(shí)灰羽,B線程執(zhí)行代碼2。此時(shí)改橘,線程A可能會(huì)看到instance引用的對(duì)象還沒有完成初始化.

對(duì)于UnsafeLazyInitialization類滋尉,我們可以對(duì)getInstance()方法做同步處理來實(shí)現(xiàn)線程安全的延遲初始化。


同步處理

由于對(duì)getInstance()方法做了同步處理飞主,synchronized將導(dǎo)致性能開銷狮惜。如果getInstance()方法被多個(gè)線程頻繁的調(diào)用,將會(huì)導(dǎo)致程序執(zhí)行性能的下降碌识。反之碾篡,如果getInstance()方法不會(huì)被多個(gè)線程頻繁的調(diào)用,那么這個(gè)延遲初始化方案將能提供令人滿意的性能筏餐。

在早期的JVM中开泽,synchronized(甚至是無競(jìng)爭(zhēng)的synchronized)存在巨大的性能開銷。因此魁瞪,人們想出了一個(gè)“聰明”的技巧:雙重檢查鎖定(Double-Checked Locking)穆律。人們想通過雙重檢查鎖定來降低同步的開銷。


导俘,如果第一次檢查instance不為null峦耘,那么就不需要執(zhí)行下面的加鎖和初始化操作。因此旅薄,可以大幅降低synchronized帶來的性能開銷辅髓。

  • 多個(gè)線程試圖在同一時(shí)間創(chuàng)建對(duì)象時(shí),會(huì)通過加鎖來保證只有一個(gè)線程能創(chuàng)建對(duì)象少梁。
  • 在對(duì)象創(chuàng)建好之后洛口,執(zhí)行g(shù)etInstance()方法將不需要獲取鎖,直接返回已創(chuàng)建好的對(duì)象凯沪。

當(dāng)線程執(zhí)行到第4行第焰,代碼讀取到instance不為null時(shí),instance引用的對(duì)象有可能還沒有完成初始化妨马。也就是可能返回的對(duì)象為空


問題的根源

前面的雙重檢查鎖定示例代碼的第7行(instance=new Singleton();)創(chuàng)建了一個(gè)對(duì)象樟遣。這一行代碼可以分解為如下的3行偽代碼。


上面3行偽代碼中的2和3之間,可能會(huì)被重排序(在一些JIT編譯器上身笤,這種重排序是真實(shí)發(fā)生的).2和3之間重排序之后的執(zhí)行時(shí)時(shí)序如下.


第三步進(jìn)行后instance就不為null了

所有線程在執(zhí)行Java程序時(shí)必須要遵守intra-thread semantics(內(nèi)部線程語義)豹悬。intra-thread semantics保證重排序不會(huì)改變單線程內(nèi)的程序執(zhí)行結(jié)果。換句話說液荸,intra-thread semantics允許那些在單線程內(nèi)瞻佛,不會(huì)改變單線程程序執(zhí)行結(jié)果的重排序。上面3行偽代碼的2和3之間雖然被重排序了,但這個(gè)重排序并不會(huì)違反intra-thread semantics伤柄。這個(gè)重排序在沒有改變單線程程序執(zhí)行結(jié)果的前提下绊困,可以提高程序的執(zhí)行性能。

假設(shè)一個(gè)線程A在構(gòu)造對(duì)象后适刀,立即訪問這個(gè)對(duì)象
多線程執(zhí)行時(shí)序圖

由于單線程內(nèi)要遵守intra-thread semantics秤朗,從而能保證A線程的執(zhí)行結(jié)果不會(huì)被改變。但是笔喉,當(dāng)線程A和B按上述多線程執(zhí)行時(shí)序圖時(shí)序執(zhí)行時(shí)取视,B線程將看到一個(gè)還沒有被初始化的對(duì)象。

DoubleCheckedLocking示例代碼的第7行(instance=new Singleton();)如果發(fā)生重排序常挚,另一個(gè)并發(fā)執(zhí)行的線程B就有可能在第4行判斷instance不為null作谭。線程B接下來將訪問instance所引用的對(duì)象,但此時(shí)這個(gè)對(duì)象可能還沒有被A線程初始化奄毡!
上述場(chǎng)景具體執(zhí)行時(shí)序

多線程執(zhí)行時(shí)序表

這里A2和A3雖然重排序了,但Java內(nèi)存模型的的intra-thread semantics將確保A2一定會(huì)排在A4前面執(zhí)行折欠。因此,線程A的intra-thread semantics沒有改變吼过,但A2和A3的重排序锐秦,將導(dǎo)致線程B在B1處判斷出instance不為空,線程B接下來將訪問instance引用的對(duì)象盗忱。此時(shí)酱床,線程B將會(huì)訪問到一個(gè)還未初始化的對(duì)象.

針對(duì)與上述雙重檢查延遲鎖定存在的問題我們可以使用以下兩種方法解決:

  • 不允許3和3進(jìn)行重排序操作
  • 允許2和3重排序,但不允許其他線程“看到”這個(gè)重排序售淡。

基于volatile的解決

對(duì)于前面的基于雙重檢查鎖定來實(shí)現(xiàn)延遲初始化的方案(指DoubleCheckedLocking示例代碼),只需要做一點(diǎn)小的修改(把instance聲明為volatile型)慷垮,就可以實(shí)現(xiàn)線程安全的延遲初始化揖闸。


注意:這個(gè)解決方案需要JDK 5或更高版本(因?yàn)閺腏DK 5開始使用新的JSR-133內(nèi)存模型規(guī)范,這個(gè)規(guī)范增強(qiáng)了volatile的語義)料身。

picture 3.0

當(dāng)聲明對(duì)象為volatile后,上述的3行偽代碼中2和3之間的重排序,在多線程環(huán)境當(dāng)中就會(huì)被禁止.上面的實(shí)例代碼就會(huì)按照下面的時(shí)序圖運(yùn)行.


這個(gè)方案本質(zhì)上是通過picture3.0中2和3之間的重排序,來保證線程安全的延遲初始化.


基于類初始化的解決方案

JVM在類的初始化階段(即在Class被加載后,且被線程使用之前),會(huì)執(zhí)行類的初始化.在執(zhí)行類的初始化期間,JVM回去獲取一個(gè)鎖.這個(gè)鎖可以同步多個(gè)線程對(duì)同一個(gè)類的初始化.

基于這個(gè)特性,可以實(shí)現(xiàn)另一種線程安全的延遲初始化方案--Initialization On Demand Holder idiom

假設(shè)兩個(gè)線程并發(fā)執(zhí)行g(shù)etInstance()方法汤纸,下面是執(zhí)行的示意圖


兩個(gè)線程并發(fā)執(zhí)行的示意圖

這個(gè)方案的實(shí)質(zhì)是:允許3.8.2節(jié)中的3行偽代碼中的2和3重排序,但不允許非構(gòu)造線程(這里指線程B)“看到”這個(gè)重排序芹血。

初始化一個(gè)類贮泞,包括執(zhí)行這個(gè)類的靜態(tài)初始化和初始化在這個(gè)類中聲明的靜態(tài)字段。根據(jù)Java語言規(guī)范幔烛,在首次發(fā)生下列任意一種情況時(shí)啃擦,一個(gè)類或接口類型T將被立即初始化。
1)T是一個(gè)類饿悬,而且一個(gè)T類型的實(shí)例被創(chuàng)建令蛉。
2)T是一個(gè)類,且T中聲明的一個(gè)靜態(tài)方法被調(diào)用。
3)T中聲明的一個(gè)靜態(tài)字段被賦值珠叔。
4)T中聲明的一個(gè)靜態(tài)字段被使用蝎宇,而且這個(gè)字段不是一個(gè)常量字
5)T是一個(gè)頂級(jí)類(Top Level Class,見Java語言規(guī)范的§7.6)祷安,而且一個(gè)斷言語句嵌套在T內(nèi)部被執(zhí)行姥芥。

在InstanceFactory示例代碼中,首次執(zhí)行g(shù)etInstance()方法的線程將導(dǎo)致InstanceHolder類被
初始化(符合情況4)汇鞭。

由于Java語言是多線程的,多個(gè)線程可能在同一時(shí)間嘗試去初始化同一個(gè)類或接口(比如這里多個(gè)線程可能在同一時(shí)刻調(diào)用getInstance()方法來初始化InstanceHolder類.因此,在Java中初始化一個(gè)類或者接口時(shí),需要做細(xì)致的處理.

Java語言規(guī)范規(guī)定,對(duì)于每一個(gè)類或接口C都有一個(gè)唯一的初始化多LC與之對(duì)應(yīng).從C到LC的映射,由JVM的具體實(shí)現(xiàn)去自由實(shí)現(xiàn).JVM在類初始化期間會(huì)獲取這個(gè)初始化鎖,并且每個(gè)線程至少獲取一次鎖來確保這個(gè)類已經(jīng)被初始化過了(事實(shí)上Java序言允許JVM的具體實(shí)現(xiàn)在這里做一些優(yōu)化).

對(duì)于類或接口的初始化.Java語言規(guī)范指制定了精巧而復(fù)雜的類初始化過程.Java初始化一個(gè)類或接口的處理過程如下.

類或接口初始化的五個(gè)階段

第一階段

通過Class對(duì)象上同步(即獲取Class對(duì)象的初始化鎖),來控制類或接口的初始化.這個(gè)獲取鎖的線程會(huì)一直等待,只到當(dāng)前線程能夠獲取到這個(gè)初始化鎖.

假設(shè)Class對(duì)象當(dāng)前還沒有初始化(初始化狀態(tài)state,此時(shí)被標(biāo)記為為state=noInitialization)凉唐,且有兩個(gè)線程A和B試圖同時(shí)初始化這個(gè)Class對(duì)象。


類初始化——第1階段

類初始化——第1階段的執(zhí)行時(shí)序表
第二階段

線程A執(zhí)行類的初始化虱咧,同時(shí)線程B在初始化鎖對(duì)應(yīng)的condition上等待

類初始化——第2階段

類初始化——第2階段的執(zhí)行時(shí)序表

第三階段

線程A設(shè)置state=initialized熊榛,然后喚醒在condition中等待的所有線程

類初始化——第3階段

類初始化——第3階段的執(zhí)行時(shí)序表

第四階段

線程B結(jié)束類的初始化處理

類初始化——第4階段

類初始化——第4階段的執(zhí)行時(shí)序表

多線程執(zhí)行時(shí)序圖

線程A在第2階段的A1執(zhí)行類的初始化,并在第3階段的A4釋放初始化鎖腕巡;線程B在第4階段的B1獲取同一個(gè)初始化鎖玄坦,并在第4階段的B4之后才開始訪問這個(gè)類。根據(jù)Java內(nèi)存模型規(guī)范的鎖規(guī)則绘沉,這里將存在如下的happens-before關(guān)系煎楣。

這個(gè)happens-before關(guān)系將保證:線程A執(zhí)行類的初始化時(shí)的寫入操作(執(zhí)行類的靜態(tài)初始化和初始化類中聲明的靜態(tài)字段),線程B一定能看到车伞。

第五階段

線程C執(zhí)行類的初始化的處理择懂。

類初始化——第5階段

類初始化——第5階段的執(zhí)行時(shí)序表

在第3階段之后,類已經(jīng)完成了初始化另玖。因此線程C在第5階段的類初始化處理過程相對(duì)簡(jiǎn)單一些(前面的線程A和B的類初始化處理過程都經(jīng)歷了兩次鎖獲取-鎖釋放困曙,而線程C的類初始化處理只需要經(jīng)歷一次鎖獲取-鎖釋放)。

線程A在第2階段的A1執(zhí)行類的初始化谦去,并在第3階段的A4釋放鎖慷丽;線程C在第5階段的C1獲取同一個(gè)鎖,并在在第5階段的C4之后才開始訪問這個(gè)類鳄哭。根據(jù)Java內(nèi)存模型規(guī)范的鎖規(guī)則要糊,將存在如下的happens-before關(guān)系。

這個(gè)happens-before關(guān)系將保證:線程A執(zhí)行類的初始化時(shí)的寫入操作妆丘,線程C一定能看到锄俄。

注意:這里的condition和state標(biāo)記是本文虛構(gòu)出來的。Java語言規(guī)范并沒有硬性規(guī)定一定要使用condition和state標(biāo)記勺拣。JVM的具體實(shí)現(xiàn)只要實(shí)現(xiàn)類似功能即可奶赠。


小結(jié)

通過對(duì)比基于volatile的雙重檢查鎖定的方案和基于類初始化的方案,我們會(huì)發(fā)現(xiàn)基于類初始化的方案的實(shí)現(xiàn)代碼更簡(jiǎn)潔药有。但基于volatile的雙重檢查鎖定的方案有一個(gè)額外的優(yōu)勢(shì):除了可以對(duì)靜態(tài)字段實(shí)現(xiàn)延遲初始化外车柠,還可以對(duì)實(shí)例字段實(shí)現(xiàn)延遲初始化。

字段延遲初始化降低了初始化類或創(chuàng)建實(shí)例的開銷,但增加了訪問被延遲初始化的字段的開銷竹祷。在大多數(shù)時(shí)候谈跛,正常的初始化要優(yōu)于延遲初始化。如果確實(shí)需要對(duì)實(shí)例字段使用線程安全的延遲初始化塑陵,請(qǐng)使用上面介紹的基于volatile的延遲初始化的方案感憾;如果確實(shí)需要對(duì)靜態(tài)字段使用線程安全的延遲初始化,請(qǐng)使用上面介紹的基于類初始化的方案令花。

參考書籍:<<Java并發(fā)編程的藝術(shù)>>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末阻桅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子兼都,更是在濱河造成了極大的恐慌嫂沉,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扮碧,死亡現(xiàn)場(chǎng)離奇詭異趟章,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)慎王,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蚓土,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赖淤,你說我怎么就攤上這事蜀漆。” “怎么了咱旱?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵确丢,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我吐限,道長(zhǎng)鲜侥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任毯盈,我火速辦了婚禮剃毒,結(jié)果婚禮上病袄,老公的妹妹穿的比我還像新娘搂赋。我一直安慰自己,他們只是感情好益缠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布脑奠。 她就那樣靜靜地躺著,像睡著了一般幅慌。 火紅的嫁衣襯著肌膚如雪宋欺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音齿诞,去河邊找鬼酸休。 笑死,一個(gè)胖子當(dāng)著我的面吹牛祷杈,可吹牛的內(nèi)容都是我干的斑司。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼但汞,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼宿刮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起私蕾,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤僵缺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后踩叭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磕潮,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年懊纳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了比然。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡叭莫,死狀恐怖摩桶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情茂缚,我是刑警寧澤戏罢,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站脚囊,受9級(jí)特大地震影響龟糕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜悔耘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一讲岁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧衬以,春花似錦缓艳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至互妓,卻和暖如春溪窒,著一層夾襖步出監(jiān)牢的瞬間坤塞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工澈蚌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留摹芙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓宛瞄,卻偏偏與公主長(zhǎng)得像瘫辩,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子坛悉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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