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();
是一條賦值語句,事實上哼审,它并不是一個原子操作谐腰。它大概會做三件事情:
- 為對象分配內(nèi)存;
- 調(diào)用對應(yīng)的構(gòu)造做對象的初始化操作涩盾;
- 將引用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)化成:
可以看到,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() {}
}