單例模式之雙重檢查的演變

前言

單例模式本身是很簡(jiǎn)單的,但是考慮到線程安全問題湾笛,簡(jiǎn)單的問題就變復(fù)雜了俯树。這里講解單例模式的雙重檢查。

單例模式演變

沒有多線程的世界

最開始的單例模式應(yīng)該是如下代碼畜挨。

public class Singleton {
    private static Singleton singleton = null;

    public static Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

然后有人說你這個(gè)是線程不安全的筒繁。為什么是線程不安全噩凹?舉個(gè)例子。
A線程由于第一次進(jìn)來由于 singleton實(shí)例為空毡咏,所以執(zhí)行new Singleton()驮宴,然后將singleton返回。由于多級(jí)緩存的存在呕缭,這個(gè)實(shí)例有可能只存在A線程的工作內(nèi)存中堵泽,并沒用立即往主內(nèi)存中寫。這個(gè)時(shí)候B線程進(jìn)來一樣判斷singleton == null恢总,B線程取singleton有兩個(gè)地方一個(gè)是自己的工作內(nèi)存迎罗,另一個(gè)是主內(nèi)存,但是都沒有片仿,因此B線程也要?jiǎng)?chuàng)建一遍singleton纹安。

工作內(nèi)存與主內(nèi)存數(shù)據(jù)同步

這個(gè)時(shí)候就有人說了程癌,我們?cè)?code>singleton加上volatile關(guān)鍵字赢笨,這樣只要有線程創(chuàng)建singleton,都會(huì)往主內(nèi)存中寫楚堤,并且所有的線程都是可見的阳距。因此就有如下代碼塔粒。

public class Singleton {
    // 加入volatile,所有線程都從主內(nèi)存中取值娄涩。
    private static volatile Singleton singleton = null;
    public static  Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

但是還是有線程安全問題窗怒,比如第一個(gè)線程在創(chuàng)建singleton實(shí)例(還沒有創(chuàng)建完成)映跟,第二個(gè)線程判斷singleton==null 為true蓄拣,所以依然會(huì)創(chuàng)建兩次singleton。這個(gè)時(shí)候就會(huì)想努隙,直接來個(gè)簡(jiǎn)單粗暴的方法球恤,直接在方法上加synchronized

簡(jiǎn)單粗暴的方法

public class Singleton {
    private static Singleton singleton = null;
    public static synchronized Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

這下總算是滿足既要單例荸镊,又要線程安全咽斧。當(dāng)一直出現(xiàn)問題的時(shí)候,人們就想著趕快給我簡(jiǎn)單粗暴的解決方案吧躬存,只要解決問題就行张惹,不管什么方法都o(jì)k。當(dāng)問題得到解決的時(shí)候岭洲,程序也運(yùn)行了一段時(shí)間宛逗,人們又想著鎖住整個(gè)方法,這也太菜了盾剩,我得想著優(yōu)化方法雷激。鎖住部分代碼替蔬。

在優(yōu)化的路上漸行漸遠(yuǎn)

但是這也會(huì)出現(xiàn)線程安全問題,如下代碼線程A進(jìn)入創(chuàng)建singleton屎暇,趁著線程A還沒有完全創(chuàng)建singleton承桥,線程B登場(chǎng),由于A還在臨界區(qū)內(nèi)根悼,B線程只能等待凶异,接著A線程執(zhí)行完,線程B進(jìn)入臨界區(qū)創(chuàng)建singleton挤巡,兩次創(chuàng)建singleton唠帝,所以線程不安全。

public class Singleton {
    private static Singleton singleton = null;
    public static  Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

public class Singleton {
    private static volatile  Singleton singleton = null;
    public static  Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

上面兩個(gè)線程不安全是因?yàn)榈诙€(gè)線程會(huì)進(jìn)入臨界區(qū)玄柏,并且創(chuàng)建singleton襟衰,所以就有人提出在臨近區(qū)也對(duì)singleton進(jìn)行判空。如下代碼粪摘,第二個(gè)線程進(jìn)入臨界區(qū)的時(shí)候判斷singleton == null 瀑晒,由于第一個(gè)線程已經(jīng)創(chuàng)建了singleton并且釋放鎖了,JMM會(huì)將線程A的singleton對(duì)象刷新到主內(nèi)存中徘意,因此第二個(gè)線程從主內(nèi)存中獲取到singletone苔悦,所以不會(huì)再次創(chuàng)建singleton實(shí)例。乍一想好像沒有什么問題椎咧,但是以下代碼還是會(huì)存在線程安全問題玖详。這是為何?

public class Singleton {
    private static  Singleton singleton = null;
    public static  Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

好生厲害的重排序

這就要牽扯到重新排序問題勤讽。為了提高運(yùn)行速度一般有以下重排序:

  1. 編譯器優(yōu)化重排序
  2. 指令級(jí)并行重排序
  3. 內(nèi)存系統(tǒng)重排序
    singleton = new Singleton() 看上去是一行代碼蟋座,其實(shí)編譯后對(duì)應(yīng)的多條字節(jié)碼指令,一條字節(jié)碼指令可能轉(zhuǎn)換多條機(jī)器指令脚牍∠蛲危可以簡(jiǎn)單的把創(chuàng)建對(duì)象的過程看做以下步驟
1. 分配對(duì)象內(nèi)存空間
2. 初始化構(gòu)造函數(shù)
3. 賦值

按照順序執(zhí)行并沒有什么毛病,但是如果出現(xiàn)重排序?qū)?code>123變成132那么對(duì)象沒有初始化完就被第二個(gè)線程看到诸狭,所以上述的代碼存在的問題是第一個(gè)線程執(zhí)行了13過程券膀,第二個(gè)線程執(zhí)行到singleton == null 這個(gè)時(shí)候由于singleton 引用已經(jīng)指向內(nèi)存中的某塊區(qū)域,所以singleton== null判斷就為false驯遇,接下來第二個(gè)線程使用沒有初始化完成的對(duì)象芹彬。這個(gè)線程安全問題是重排序?qū)е碌模虼酥灰鉀Q重排序問題叉庐,就解決了該代碼的線程安全問題舒帮,這個(gè)時(shí)候就該引入volatile,被volatile的字段寫入的時(shí)候,JMM會(huì)在寫入前插入一個(gè)StoreStore屏障会前,意思就是寫volatile字段之前好乐,前面的寫操作都已完成,在這里這個(gè)屏障就是插入在23之間瓦宜,因此singletonvolatile修飾后蔚万,指令執(zhí)行3操作前12操作都已完成。所以其他線程只要看到了singleton對(duì)象临庇,那么該對(duì)象就一定初始化完成反璃,因此這就解決了線程安全問題。

public class Singleton {
    private static volatile  Singleton singleton = null;
    public static  Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

總結(jié)

這里簡(jiǎn)單總結(jié)下單例模式的線程安全問題假夺。之所以出現(xiàn)安全問題有如下原因:

  1. 多級(jí)緩存與主內(nèi)存的值不同步淮蜈。
  2. 重排序問題。
  3. 對(duì)臨界區(qū)的訪問問題已卷。
    因此只要解決上述三個(gè)問題梧田,基本上就解決單例模式的線程安全問題。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末侧蘸,一起剝皮案震驚了整個(gè)濱河市裁眯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌讳癌,老刑警劉巖穿稳,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異晌坤,居然都是意外死亡逢艘,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門骤菠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來它改,“玉大人,你說我怎么就攤上這事娩怎∩危” “怎么了胰柑?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵截亦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我柬讨,道長(zhǎng)崩瓤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任踩官,我火速辦了婚禮却桶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己颖系,他們只是感情好嗅剖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嘁扼,像睡著了一般信粮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趁啸,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天强缘,我揣著相機(jī)與錄音,去河邊找鬼不傅。 笑死旅掂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的访娶。 我是一名探鬼主播商虐,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼崖疤!你這毒婦竟也來了称龙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤戳晌,失蹤者是張志新(化名)和其女友劉穎鲫尊,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沦偎,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疫向,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了豪嚎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搔驼。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖侈询,靈堂內(nèi)的尸體忽然破棺而出舌涨,到底是詐尸還是另有隱情,我是刑警寧澤扔字,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布囊嘉,位于F島的核電站,受9級(jí)特大地震影響革为,放射性物質(zhì)發(fā)生泄漏扭粱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一震檩、第九天 我趴在偏房一處隱蔽的房頂上張望琢蛤。 院中可真熱鬧蜓堕,春花似錦、人聲如沸博其。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)慕淡。三九已至霜旧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間儡率,已是汗流浹背挂据。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留儿普,地道東北人崎逃。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像眉孩,于是被迫代替她去往敵國(guó)和親个绍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

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