雙重檢查鎖為什么要使用volatile字段?

雙重鎖的由來

單例模式中,有一個DCL(雙重鎖)的實現(xiàn)方式玄柠。在Java程序中,有時候可能需要推遲一些高開銷的對象初始化操作,并且只有在使用這些對象時才開始初始化岖食。

下面是非線程安全的延遲初始化對象的實例代碼岩齿。

/**
 * @author xiaoshu
 */
public class Instance {
}

/**
 * 非線程安全的延遲初始化對象
 *
 * @author xiaoshu
 */
public class UnsafeLazyInitialization {
    private static Instance instance;

    public static Instance getInstance() {
        if (null == instance) {
            instance = new Instance();
        }
        return instance;
    }
}

在UnsafeLazyInitialization類中入篮,假設(shè)A線程執(zhí)行代碼1的同時谨垃,B線程執(zhí)行代碼2。此時墓拜,線程A可能會看到instance引用對象還沒有完成初始化港柜。

對于UnsafeLazyInitialization類,我們可以對getInstance()方法做同步處理來實現(xiàn)線程安全的延遲初始化咳榜。示例代碼如下夏醉。

/**
 * 安全的延遲初始化
 *
 * @author xiaoshu
 */
public class SafeLazyInitialization {
    private static Instance instance;

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

由于對getInstance()方法做了同步處理,synchronized將導(dǎo)致性能開銷涌韩。如果getInstance()方法被多個線程頻繁的調(diào)用授舟,將會導(dǎo)致程序執(zhí)行性能的下降。反之贸辈,如果getInstance()方法不會被多個線程頻繁的調(diào)用,那么這個延遲初始化方案將能提供令人滿意的性能肠槽。

后來擎淤,提出了一個“聰明”的技巧:雙重檢查鎖定(Double-Checked Locking)。想通過雙重檢查鎖定來降低同步的開銷秸仙。下面是使用雙重檢查鎖定來實現(xiàn)延遲初始化的實例代碼嘴拢。

/**
 * 雙重檢查鎖定
 *
 * @author xiaoshu
 */
public class DoubleCheckedLocking {
    private static Instance instance;

    public static Instance getInstance() {
        if (null == instance) {                             //1.第一次檢查
            synchronized (DoubleCheckedLocking.class) {     //2.加鎖
                if (null == instance) {                     //3:第二次檢查
                    instance = new Instance();              //4.問題的根源出在這里
                }
            }
        }
        return instance;
    }
}

雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優(yōu)化寂纪!在線程執(zhí)行到第1處席吴,代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化捞蛋。

問題的根源

前面的雙重檢查鎖定實例代碼的第4處(instance = new Instance();)創(chuàng)建了一個對象孝冒。這一行代碼可以分解為如下的3行偽代碼。

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

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

memory = allocate();    //1.分配對象的內(nèi)存空間
instance = memory;      //3.設(shè)置instance指向剛分配的內(nèi)存地址
                                            //注意搬设,此時對象還沒有被初始化穴店!
ctorInstance(memory); //2.初始化對象

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

時間 線程A 線程B
T1 A1:分配對象的內(nèi)存空間
T2 A3:設(shè)置instance指向內(nèi)存空間
T3 B1:判斷instance是否為空
T4 B2:由于instance不為null撕捍,線程B將訪問instance引用的對象
T5 A2:初始化對象
T6 A4:訪問instance引用的對象

在知曉了問題發(fā)生的根源之后,我們可以想出兩個方法來實現(xiàn)線程安全的延遲初始化泣洞。

1)不允許2和3重排序

2)允許2和3重排序忧风,但不允許其他線程“看到”這個重排序。

后文介紹的兩個解決方案球凰,分別對應(yīng)于上面這兩點狮腿。

解決方案一:基于volatile的解決方案

/**
 * 安全的雙重檢查鎖定
 *
 * @author xiaoshu
 */
public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if (null == instance) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (null == instance) {
                    instance = new Instance();//instance為volatile,現(xiàn)在沒有問題了弟蚀。
                }
            }
        }
        return instance;
    }
}

注意:這個解決方案需要JDK5或更高版本(因為從JDK5開始使用新的JSR-133內(nèi)存模型規(guī)范蚤霞,這個規(guī)范增強了volatile的語義)。

當聲明對象的引用為volatile后义钉,3行偽代碼中的2和3之間的重排序昧绣,在多線程環(huán)境中將會被禁止。

解決方案二:基于類初始化的解決方案

JVM在類的初始化階段(即在Class被加載后捶闸,且被線程使用之前)夜畴,會執(zhí)行類的初始化。在執(zhí)行類的初始化期間删壮,JVM會去獲取一個鎖.這個鎖可以同步多個線程對同一個類的初始化贪绘。

基于這個特性,可以實現(xiàn)另一種線程安全的延遲初始化方案(這個方案被稱之為Initialization On Demand Holder idiom)央碟。

/**
 * 基于類初始化的解決方案
 *
 * @author xiaoshu
 */
public class InstanceFactory {
    private static class InstanceHolder {
        private static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance; //這里將導(dǎo)致InstanceHolder類被初始化
    }
}

字段延遲初始化降低了初始化類或創(chuàng)建實例的開銷税灌,但增加了訪問被延遲初始化的字段的開銷。在大多數(shù)時候亿虽,正常的初始化要優(yōu)于延遲初始化菱涤。如果確實需要對實例字段使用線程安全的延遲初始化,請使用上面介紹的基于volatile的延遲初始化的方案洛勉;如果確實需要對靜態(tài)字段使用線程安全的延遲初始化粘秆,請使用上面介紹的基于類初始化的方案。

參考:

  1. 《Java并發(fā)編程的藝術(shù)》
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末收毫,一起剝皮案震驚了整個濱河市攻走,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌此再,老刑警劉巖昔搂,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異引润,居然都是意外死亡巩趁,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來议慰,“玉大人蠢古,你說我怎么就攤上這事”鸢迹” “怎么了草讶?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長炉菲。 經(jīng)常有香客問我堕战,道長,這世上最難降的妖魔是什么拍霜? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任嘱丢,我火速辦了婚禮,結(jié)果婚禮上祠饺,老公的妹妹穿的比我還像新娘越驻。我一直安慰自己,他們只是感情好道偷,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布缀旁。 她就那樣靜靜地躺著,像睡著了一般勺鸦。 火紅的嫁衣襯著肌膚如雪并巍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天换途,我揣著相機與錄音懊渡,去河邊找鬼。 笑死军拟,一個胖子當著我的面吹牛距贷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吻谋,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼现横!你這毒婦竟也來了漓拾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤戒祠,失蹤者是張志新(化名)和其女友劉穎骇两,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姜盈,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡低千,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片示血。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡棋傍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出难审,到底是詐尸還是另有隱情瘫拣,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布告喊,位于F島的核電站麸拄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏黔姜。R本人自食惡果不足惜拢切,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望秆吵。 院中可真熱鬧淮椰,春花似錦、人聲如沸帮毁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烈疚。三九已至黔牵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間爷肝,已是汗流浹背猾浦。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留灯抛,地道東北人金赦。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像对嚼,于是被迫代替她去往敵國和親夹抗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

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