雙重檢查鎖定與延遲初始化

在Java多線程程序中丹弱,有時(shí)候需要采用延遲初始化來(lái)降低初始化類和創(chuàng)建對(duì)象的開(kāi)銷(xiāo)德撬。雙重檢查鎖定是常見(jiàn)的延遲初始化技術(shù),但它是一個(gè)錯(cuò)誤的用法躲胳。本文將分析雙重檢查鎖定的錯(cuò)誤根源蜓洪,以及兩種線程安全的延遲初始化方案。

雙重檢查鎖定的由來(lái)

在Java程序中坯苹,有時(shí)候可能需要推遲一些高開(kāi)銷(xiāo)的對(duì)象初始化操作隆檀,并且只有在使用這些對(duì)象時(shí)才進(jìn)行初始化。此時(shí)粹湃,程序員可能會(huì)采用延遲初始化恐仑。但要正確實(shí)現(xiàn)線程安全的延遲初始化需要一些技巧,否則很容易出現(xiàn)問(wèn)題为鳄。比如裳仆,下面是非線程安全的延遲初始化對(duì)象的示例代碼。

public class UnsafeLazyInitialization {
    private static Instance instance;
    
    public static Instance getInstance() {
        if (instance == null) {         //1: A線程執(zhí)行
            instance = new Instance();  //2: B線程執(zhí)行
        }
        return instance;
    }
}

在UnsafeLazyInitialization類中孤钦,假設(shè)A線程執(zhí)行代碼1的同時(shí)歧斟,B線程執(zhí)行代碼2。此時(shí)偏形,線程A可能會(huì)看到instance引用的對(duì)象還沒(méi)有完成初始化构捡。

對(duì)于UnsafeLazyInitialization類,我們可以對(duì)getInstance()方法做同步處理來(lái)實(shí)現(xiàn)線程安全的延遲初始化壳猜。示例代碼如下勾徽。

public class SafeLazyInitialization {
private static Instance instance;
    
    public synchronized static Instance getInstance() {
        if (instance == null) {         
            instance = new Instance();  
        }
        return instance;
    } 
}

由于對(duì)getInstance()方法做了同步處理,synchronized將導(dǎo)致性能開(kāi)銷(xiāo)统扳。如果getInstance()方法被多個(gè)線程頻繁的調(diào)用喘帚,將會(huì)導(dǎo)致程序執(zhí)行性能的下降。反之咒钟,如果getInstance()方法不會(huì)被多個(gè)線程頻繁的調(diào)用吹由,那么這個(gè)延遲初始化方案將能提供令人滿意的性能。

在早期的JVM中朱嘴,synchronized(甚至是無(wú)競(jìng)爭(zhēng)的synchronized)存在巨大的性能開(kāi)銷(xiāo)倾鲫。因此粗合,人們想出了一個(gè)“聰明”的技巧:雙重檢查鎖定(Double-Checked Locking)。人們想通過(guò)雙重檢查鎖定來(lái)降低同步的開(kāi)銷(xiāo)乌昔。下面是使用雙重檢查鎖定來(lái)實(shí)現(xiàn)延遲初始化的示例代碼隙疚。

public class DoubleCheckedLocking {                         //1
    private static Instance instance;                       //2
    public static Instance getInstance() {                  //3
        if (instance == null) {                             //4:第一次檢查
            synchronized (DoubleCheckedLocking.class) {     //5: 加鎖
                if (instance == null) {                     //6: 第二次檢查
                    instance = new Instance();              //7: 問(wèn)題的根源出在這里
                }                                           //8
            }                                               //9
        }                                                   //10
        return instance;                                    //11
    }
}

如上面代碼所示,如果第一次檢查instance不為null磕道,那么就不需要執(zhí)行下面的加鎖和初始化操作供屉。因此,可以大幅降低synchronized帶來(lái)的性能開(kāi)銷(xiāo)溺蕉。上面代碼表面上看起來(lái)伶丐,似乎兩全其美。

  • 多個(gè)線程試圖在同一時(shí)間創(chuàng)建對(duì)象時(shí)疯特,會(huì)通過(guò)加鎖來(lái)保證只有一個(gè)線程能創(chuàng)建對(duì)象哗魂。
  • 在對(duì)象創(chuàng)建好之后,執(zhí)行g(shù)etInstance()方法將不需要獲取鎖漓雅,直接返回已創(chuàng)建好的對(duì)象录别。

雙重檢查鎖定看起來(lái)似乎很完美,但這是一個(gè)錯(cuò)誤的優(yōu)化故硅!在線程執(zhí)行到第一次檢查時(shí),代碼讀取到instance不為null時(shí)纵搁,instance引用的對(duì)象有可能還沒(méi)有完成初始化吃衅。

問(wèn)題的根源

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

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

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

memory = allocate(); //1:分配對(duì)象的內(nèi)存空間
instance = memory;   //3:設(shè)置instance指向剛分配的內(nèi)存地址 //注意,此時(shí)對(duì)象還沒(méi)有被初始化猪贪!
ctorInstance(memory); //2:初始化對(duì)象

根據(jù)《The Java Language Specif ication, Java SE 7 Edition》(簡(jiǎn)稱為Java語(yǔ)言規(guī)范)跷敬,所有線程在執(zhí)行Java程序時(shí)必須要遵守intra-thread semantics。intra-thread semantics保證重排序不會(huì)改變單線程內(nèi)的程序執(zhí)行結(jié)果热押。換句話說(shuō)西傀,intra-thread semantics允許那些在單線程內(nèi),不會(huì)改變單線程程序執(zhí)行結(jié)果的重排序桶癣。上面3行偽代碼的2和3之間雖然被重排序了拥褂,但這個(gè)重排序并不會(huì)違反intra-thread semantics。這個(gè)重排序在沒(méi)有改變單線程程序執(zhí)行結(jié)果的前提下牙寞,可以提高程序的執(zhí)行性能饺鹃。

線程執(zhí)行時(shí)序
1: 分配對(duì)象的內(nèi)存空間
3: 設(shè)置instance指向內(nèi)存空間
2: 初始化對(duì)象
3: 初次訪問(wèn)對(duì)象

雖然這里2和3重排序了,但是只要保證2排在4的前面執(zhí)行,單線程內(nèi)的執(zhí)行結(jié)果就不會(huì)被改變

多線程執(zhí)行時(shí)序如下:

線程A 線程B
1: 分配對(duì)象的內(nèi)存空間
3:設(shè)置instance指向內(nèi)存空間
- 判斷instance是否為null
- B線程初次訪問(wèn)對(duì)象
2:初始化對(duì)象
4:A線程初次訪問(wèn)對(duì)象

由于單線程內(nèi)要遵守intra-thread semantics悔详,從而能保證A線程的執(zhí)行結(jié)果不會(huì)被改變镊屎。但是,當(dāng)線程A和B按如上時(shí)序執(zhí)行時(shí)伟端,B線程將看到一個(gè)還沒(méi)有被初始化的對(duì)象杯道。

回到本文的主題,DoubleCheckedLocking示例代碼的第7行(instance=newSingleton();)如果發(fā)生重排序责蝠,另一個(gè)并發(fā)執(zhí)行的線程B就有可能在第4行判斷instance不為null党巾。線程B接下來(lái)將訪問(wèn)instance所引用的對(duì)象,但此時(shí)這個(gè)對(duì)象可能還沒(méi)有被A線程初始化霜医!下表是這個(gè)場(chǎng)景的具體執(zhí)行時(shí)序齿拂。

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

時(shí)間 線程A 線程B
t1 A1: 分配對(duì)象的內(nèi)存空間 -
t2 A3: 設(shè)置instance指向內(nèi)存空間 -
t3 - B1: 判斷instance是否為空
t4 - B2: 由于instance不為null,線程B將訪問(wèn)instance引用的對(duì)象
t5 A2: 初始化對(duì)象 -
t6 A4: 訪問(wèn)instance引用的對(duì)象 -

這里A2和A3雖然重排序了,但Java內(nèi)存模型的intra-thread semantics將確保A2一定會(huì)排在A4前面執(zhí)行肴敛。因此署海,線程A的intra-thread semantics沒(méi)有改變,但A2和A3的重排序医男,將導(dǎo)致線程B在B1處判斷出instance不為空砸狞,線程B接下來(lái)將訪問(wèn)instance引用的對(duì)象。此時(shí)镀梭,線程B將會(huì)訪問(wèn)到一個(gè)還未初始化的對(duì)象刀森。

在知曉了問(wèn)題發(fā)生的根源之后,我們可以想出兩個(gè)辦法來(lái)實(shí)現(xiàn)線程安全的延遲初始化报账。

  1. 不允許2和3重排序研底。
  2. 允許2和3重排序,但不允許其他線程“看到”這個(gè)重排序透罢。
    后文介紹的兩個(gè)解決方案榜晦,分別對(duì)應(yīng)于上面這兩點(diǎn)。

基于volatile的解決方案

將實(shí)例定義為volatile類型羽圃,實(shí)現(xiàn)線程安全的延遲初始化乾胶。

public class SafeDoubleCheckLocking {
    private volatile static Instance instance;
    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckLocking.class) {
                if (instance == null) {
                    instance = new Instance();  //instance為volatile,現(xiàn)在沒(méi)問(wèn)題了
                }
            }
        }
        return instance;
    }
}

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

基于類初始化的解決方案

JVM在類的初始化階段(即在Class被加載后胚吁,且被線程使用之前),會(huì)執(zhí)行類的初始化愁憔。在執(zhí)行類的初始化期間腕扶,JVM會(huì)去獲取一個(gè)鎖。這個(gè)鎖可以同步多個(gè)線程對(duì)同一個(gè)類的初始化吨掌。
基于這個(gè)特性半抱,可以實(shí)現(xiàn)另一種線程安全的延遲初始化方案(這個(gè)方案被稱之為Initialization On Demand Holder idiom)脓恕。

public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }
    
    public static Instance getInstance() {
        return InstanceHolder.instance;     //這里導(dǎo)致InstanceHolder初始化
    }
}

這個(gè)方案的實(shí)質(zhì)是:允許臨界區(qū)代碼重排序,但不允許非構(gòu)造線程(這里指線程B)“看到”這個(gè)重排序窿侈。

初始化一個(gè)類炼幔,包括執(zhí)行這個(gè)類的靜態(tài)初始化和初始化在這個(gè)類中聲明的靜態(tài)字段。根據(jù)Java語(yǔ)言規(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í)類(其他類外面聲明的類)愈污,而且一個(gè)斷言語(yǔ)句嵌套在T內(nèi)部被執(zhí)行。

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

由于Java語(yǔ)言是多線程的,多個(gè)線程可能在同一時(shí)間嘗試去初始化同一個(gè)類或接口(比如這里多個(gè)線程可能在同一時(shí)刻調(diào)用getInstance()方法來(lái)初始化InstanceHolder類)创夜。因此杭跪,在Java中初始化一個(gè)類或者接口時(shí),需要做細(xì)致的同步處理挥下。
Java語(yǔ)言規(guī)范規(guī)定揍魂,對(duì)于每一個(gè)類或接口C桨醋,都有一個(gè)唯一的初始化鎖LC與之對(duì)應(yīng)棚瘟。從C到LC的映射,由JVM的具體實(shí)現(xiàn)去自由實(shí)現(xiàn)喜最。JVM在類初始化期間會(huì)獲取這個(gè)初始化鎖偎蘸,并且每個(gè)線程至少獲取一次鎖來(lái)確保這個(gè)類已經(jīng)被初始化過(guò)了(事實(shí)上,Java語(yǔ)言規(guī)范允許JVM的具體實(shí)現(xiàn)在這里做一些優(yōu)化)瞬内。

總結(jié)

通過(guò)對(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í)例的開(kāi)銷(xiāo),但增加了訪問(wèn)被延遲初始化的字段的開(kāi)銷(xiāo)能真。在大多數(shù)時(shí)候赁严,正常的初始化要優(yōu)于延遲初始化扰柠。

如果確實(shí)需要對(duì)實(shí)例字段使用線程安全的延遲初始化,請(qǐng)使用上面介紹的基于volatile的延遲初始化的方案疼约;如果確實(shí)需要對(duì)靜態(tài)字段使用線程安全的延遲初始化卤档,請(qǐng)使用上面介紹的基于類初始化的方案。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末程剥,一起剝皮案震驚了整個(gè)濱河市劝枣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌织鲸,老刑警劉巖舔腾,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異昙沦,居然都是意外死亡琢唾,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)盾饮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)采桃,“玉大人,你說(shuō)我怎么就攤上這事丘损∑瞻欤” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵徘钥,是天一觀的道長(zhǎng)衔蹲。 經(jīng)常有香客問(wèn)我,道長(zhǎng)呈础,這世上最難降的妖魔是什么舆驶? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮而钞,結(jié)果婚禮上沙廉,老公的妹妹穿的比我還像新娘。我一直安慰自己臼节,他們只是感情好撬陵,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著网缝,像睡著了一般巨税。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上粉臊,一...
    開(kāi)封第一講書(shū)人閱讀 49,829評(píng)論 1 290
  • 那天草添,我揣著相機(jī)與錄音,去河邊找鬼扼仲。 笑死远寸,一個(gè)胖子當(dāng)著我的面吹牛促王,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播而晒,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蝇狼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了倡怎?” 一聲冷哼從身側(cè)響起迅耘,我...
    開(kāi)封第一講書(shū)人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎监署,沒(méi)想到半個(gè)月后颤专,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钠乏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年栖秕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晓避。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡簇捍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俏拱,到底是詐尸還是另有隱情暑塑,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布锅必,位于F島的核電站事格,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏搞隐。R本人自食惡果不足惜驹愚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望劣纲。 院中可真熱鬧逢捺,春花似錦、人聲如沸味廊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)余佛。三九已至,卻和暖如春窍荧,著一層夾襖步出監(jiān)牢的瞬間辉巡,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工蕊退, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留郊楣,地道東北人憔恳。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像净蚤,于是被迫代替她去往敵國(guó)和親钥组。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

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