從Java實現(xiàn)Singleton模式的一個bug到Java內(nèi)存模型

Singleton模式可以說是一個非常常見而簡單的設(shè)計模式了。《設(shè)計模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》中介紹,Singleton模式的用意是:保證運行環(huán)境中只有一個目標(biāo)類的實例,并提供一個全局的接口獲得這個實例继榆。

在我的Github repo conndots/design-pattern-in-action中的singleton目錄中記錄了python、ruby、java的單例實現(xiàn)方法。這里主要介紹Java的單例實現(xiàn)方法顿锰。《如何正確地寫出單例模式》這篇博客里介紹了用java多種實現(xiàn)單例模式的方法,本文也有參考里面的實現(xiàn)。

一個看似沒有問題的實現(xiàn)

public Class SingletonWithDoubleCheckedLockingUnsafeEdition {
        private static SingletonWithDoubleCheckedLockingUnsafeEdition INSTANCE = null;
        private static final Object LOCK = new Object();

        public static SingletonWithDoubleCheckedLockingUnsafeEdition getInstance() {
            if (INSTANCE == null) {
                synchronized(LOCK) {
                    if (INSTANCE == null) {
                        INSTANCE = new SingletonWithDoubleCheckedLockingUnsafeEdition();
                    }
                }
            }
            return INSTANCE;
        }
        
        private SingletonWithDoubleCheckedLockingUnsafeEdition() {}
}

這段實現(xiàn)也是我一直實現(xiàn)單例的方法贞铣,叫雙重檢驗鎖的方法。比起使用方法的synchronized關(guān)鍵字更加高效望艺,與在靜態(tài)域直接初始化對象相比相艇,實現(xiàn)了懶加載(lazy initialization)。然并卵姜凄,這是一種有潛在問題的實現(xiàn)政溃。
這段程序是想要做:首先,判斷INSTANCE是否為空态秧,否的話董虱,無需加鎖直接獲取對象;否則申鱼,進入同步域愤诱,這時只有一個線程在同步域內(nèi)。但是捐友,在等待或者進入同步域過程中淫半,可能INSTANCE已經(jīng)被初始化賦值了,所以再次判斷INSTANCE是否為空匣砖,防止生成類的多個對象科吭,違背單例的原則。初始化完成后猴鲫,退出同步域对人,返回這個對象。

人生若只如初見拂共,一切都那么美好牺弄。

然并卵。

Java的指令重排序優(yōu)化

在計算機中匣缘,軟件系統(tǒng)與硬件系統(tǒng)的一個共同目標(biāo)是猖闪,在不改變程序運行結(jié)果的前提下,盡可能地提高并行度肌厨。編譯器培慌、處理器也遵循這樣一個目標(biāo)。

不同的指令間可能存在數(shù)據(jù)依賴柑爸。比如下面計算圓的面積的語句:

double r = 2.3d; //(1)
double pi = 3.1415926; //(2)
double area = pi * r * r; //(3)

area的計算依賴于r與pi兩個變量的賦值指令吵护。而r與pi無依賴關(guān)系。

as-if-serial語義是指:不管如何重排序(編譯器與處理器為了提高并行度),(單線程)程序的結(jié)果不能被改變馅而。這是編譯器祥诽、Runtime、處理器必須遵守的語義瓮恭。

雖然雄坪,(1) - happens before -> (2),(2) - happens before -> (3),但是計算順序(1)(2)(3)與(2)(1)(3) 對于r屯蹦、pi维哈、area變量的結(jié)果并無區(qū)別。編譯器登澜、Runtime在優(yōu)化時可以根據(jù)情況重排序(1)與(2)阔挠,而絲毫不影響程序的結(jié)果。

當(dāng)然脑蠕,這里說的重排序優(yōu)化是正對字節(jié)碼指令的购撼。這樣造成的幻覺就是,我們寫的單線程程序都是線性執(zhí)行的谴仙,as-if-serial語義使得程序員無需擔(dān)心重排序干擾代碼的邏輯迂求,也不需擔(dān)心內(nèi)存的可見性。

指令重排序優(yōu)化會影響初始化對象嗎

我們說的是指令重排序狞甚∷ぃ看起來INSTANCE = new SingletonWithDoubleCheckedLockingUnsafeEdition();是一條賦值語句,事實上哼审,它并不是一個原子操作谐腰。它大概會做三件事情:

  1. 為對象分配內(nèi)存;
  2. 調(diào)用對應(yīng)的構(gòu)造做對象的初始化操作涩盾;
  3. 將引用INSTANCE指向新分配的空間十气。

這里并沒有細(xì)化到指令的級別,但我們?nèi)匀豢梢苑治龀鋈齻€操作的依賴性: 2依賴于1春霍,3依賴于1砸西。第二步與第三步是獨立無依賴的,是可以被優(yōu)化重排序的址儒。

Nani???

我們看看按照1->3->2的順序執(zhí)行會發(fā)生什么芹枷。

線程1:getInstance()
線程1:判斷INSTANCE是否為空?Y
線程1:獲取同步鎖
線程1:判斷INSTANCE是否為空? Y
線程1:為新對象分配內(nèi)存
線程1:將引用INSTANCE指向新分配的空間莲趣。
線程2:getInstance()
線程2:判斷INSTANCE是否為空? N
線程2:返回INSTANCE對象 (擦鸳慈。INSTANCE表示老子還沒被初始化呢)
線程2:使用INSTANCE對象時發(fā)現(xiàn)這貨不能用,bug found!
線程1:調(diào)用對應(yīng)構(gòu)造器作對象初始化操作喧伞。

我們說的是多線程環(huán)境下的執(zhí)行走芋,當(dāng)然不會像上面那樣的線性過程绩郎,我想你懂我的意思的。這樣的bug不是一定會出現(xiàn)翁逞,卻是一個不小的隱患肋杖。

幸好,我們有volatile關(guān)鍵字提供內(nèi)存屏障

大家對volatile關(guān)鍵字可能更多的印象是內(nèi)存的可見性和提供的原子性挖函。一個變量被聲明為volatile后状植,在不同的線程的緩存中不會有副本,保證一致性挪圾。對聲明volatile的變量的任意讀都可以見到任意線程對這個volatile變量的寫入浅萧。對于加有volatile的變量,可以保證對它讀寫的原子性哲思。

而實際上,volatile的內(nèi)存語義可以小結(jié)如下(詳細(xì)的解釋可見:《深入理解Java內(nèi)存模型(四)——volatile》):

  • 線程A寫一個volatile變量吩案,實質(zhì)上是線程A向接下來將要讀這個volatile變量的某個線程發(fā)出了(其對共享變量所在修改的)消息棚赔。
  • 線程B讀一個volatile變量,實質(zhì)上是線程B接收了之前某個線程發(fā)出的(在寫這個volatile變量之前對共享變量所做修改的)消息徘郭。
  • 線程A寫一個volatile變量靠益,隨后線程B讀這個volatile變量,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息残揉。

然而胧后,java存在著指令重排序優(yōu)化的可能。Java內(nèi)存模型規(guī)定多種情況下不允許指令重排序抱环。

為了實現(xiàn)volatile的內(nèi)存語義壳快,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序镇草。

大多數(shù)的處理器都支持內(nèi)存屏障的指令眶痰。

對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能梯啤,為此竖伯,Java內(nèi)存模型采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略:

  • 在每個volatile寫操作的前面插入一個StoreStore屏障因宇。
  • 在每個volatile寫操作的后面插入一個StoreLoad屏障七婴。
  • 在每個volatile讀操作的后面插入一個LoadLoad屏障。
  • 在每個volatile讀操作的后面插入一個LoadStore屏障察滑。

在x86處理器平臺上打厘,保守的讀寫策略會被優(yōu)化成:

image

可以看到,x86只對寫-讀操作做了內(nèi)存屏障杭棵。在其上對volatile的寫操作比讀操作開銷大婚惫。

在一次volatile變量的寫操作后氛赐,會添加StoreLoad屏障,保證任何對volatile變量的讀操作不會被放到1->2->3或者1->3->2操作之前先舷,這樣艰管,實現(xiàn)了對象初始化過程的完整的原子性。

正確的姿勢實現(xiàn)單例模式

只需要在INSTANCE變量加上volatile關(guān)鍵字的聲明蒋川。代碼如下:

public class SingletonWithDoubleCheckedLockingFineEdition {
        private static volatile SingletonWithDoubleCheckedLockingFineEdition INSTANCE = null;
        private static final Object LOCK = new Object();

        public static SingletonWithDoubleCheckedLockingFineEdition getInstance() {
            if (INSTANCE == null) {
                synchronized(LOCK) {
                    if (INSTANCE == null) {
                        INSTANCE = new SingletonWithDoubleCheckedLockingFineEdition();
                    }
                }
            }
            return INSTANCE;
        }

        private SingletonWithDoubleCheckedLockingFineEdition() {}
}

References:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牲芋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子捺球,更是在濱河造成了極大的恐慌缸浦,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氮兵,死亡現(xiàn)場離奇詭異裂逐,居然都是意外死亡,警方通過查閱死者的電腦和手機泣栈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門卜高,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人南片,你說我怎么就攤上這事掺涛。” “怎么了疼进?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵薪缆,是天一觀的道長。 經(jīng)常有香客問我伞广,道長拣帽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任赔癌,我火速辦了婚禮诞外,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灾票。我一直安慰自己峡谊,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布刊苍。 她就那樣靜靜地躺著既们,像睡著了一般。 火紅的嫁衣襯著肌膚如雪正什。 梳的紋絲不亂的頭發(fā)上啥纸,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音婴氮,去河邊找鬼斯棒。 笑死盾致,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的荣暮。 我是一名探鬼主播庭惜,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼穗酥!你這毒婦竟也來了护赊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤砾跃,失蹤者是張志新(化名)和其女友劉穎骏啰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抽高,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡判耕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了厨内。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祈秕。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖雏胃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情志鞍,我是刑警寧澤瞭亮,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站固棚,受9級特大地震影響统翩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜此洲,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一厂汗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呜师,春花似錦娶桦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至知牌,卻和暖如春祈争,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背角寸。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工菩混, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留忿墅,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓沮峡,卻偏偏與公主長得像疚脐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子帖烘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

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