鉆鉆 “單例模式” 的牛角尖笨枯!你寫的安全嗎?

文章轉(zhuǎn)載自公眾號??秉心說?遇西,?作者 秉心說

枚舉很適合用來實現(xiàn)單例模式馅精。實際上,在 Effective Java 中也提到過(果然英雄所見略同):

單元素的枚舉類型經(jīng)常成為實現(xiàn) Singleton 的最佳方法 粱檀。

首先什么是單例洲敢?就一條基本原則,單例對象的類只會被初始化一次茄蚯。在 Java 中压彭,我們可以說在 JVM 中只存在該類的唯一一個對象實例。在 Android 中渗常,我們可以說在程序運行期間壮不,該類有且僅有一個對象實例。說到單例模式的實現(xiàn)皱碘,你們肯定信手拈來忆畅,什么懶漢,餓漢,DCL家凯,靜態(tài)內(nèi)部類缓醋,門清。在說單例之前绊诲,考慮下面幾個問題:

你的單例線程安全嗎?

你的單例反射安全嗎送粱?

你的單例序列化安全嗎?

今天掂之,我就來鉆鉆牛角尖抗俄,看看你們的單例是否真的 “單例”。

一世舰、單例的一般實現(xiàn)

1动雹、餓漢式

publicclassHungrySingleton{

privatestaticfinalHungrySingleton?mInstance?=newHungrySingleton();

privateHungrySingleton(){

}

publicstaticHungrySingletongetInstance(){

returnmInstance;

}

}

私有構(gòu)造器是單例的一般套路,保證不能在外部新建對象跟压。餓漢式在類加載時期就已經(jīng)初始化實例胰蝠,由于類加載過程是線程安全的,所以餓漢式默認(rèn)也是線程安全的震蒋。它的缺點也很明顯茸塞,我真正需要單例對象的時機(jī)是我調(diào)用?getInstance()?的時候,而不是類加載時期查剖。如果單例對象是很耗資源的钾虐,如數(shù)據(jù)庫,socket 等等笋庄,無疑是不合適的效扫。于是就有了懶漢式。

2直砂、懶漢式

publicclassLazySingleton{

privatestaticLazySingleton?mInstance;

privateLazySingleton(){

}

publicstaticsynchronizedLazySingletongetInstance(){

if(mInstance?==null)

mInstance?=newLazySingleton();

returnmInstance;

}

}

實例化的時機(jī)挪到了getInstance()?方法中荡短,做到了 lazy init ,但也失去了類加載時期初始化的線程安全保障哆键。因此使用了?synchronized?關(guān)鍵字來保障線程安全掘托。但這顯然是一個無差別攻擊,管你要不要同步籍嘹,管你是不是多線程闪盔,一律給我加鎖。這也帶來了額外的性能消耗辱士。這點問題肯定難不倒程序員們泪掀,于是,雙重檢查鎖定(DCL, Double Check Lock) 應(yīng)運而生颂碘。

3异赫、DCL

publicclassDCLSingleton{

privatestaticDCLSingleton?mInstance;

privateDCLSingleton(){

}

publicstaticDCLSingletongetInstance(){

if(mInstance?==null)?{//?1

synchronized(DCLSingleton.class)?{//?2

if(mInstance?==null)//?3

mInstance?=newDCLSingleton();//?4

}

}

returnmInstance;

}

}

1?處做第一次判斷,如果已經(jīng)實例化了,直接返回對象塔拳,避免無用的同步消耗鼠证。2?處僅對實例化過程做同步操作,保證單例靠抑。3處做第二次判斷量九,只有mInstance?為空時再初始化∷瘫蹋看起來時多么的完美荠列,保證線程安全的同時又兼顧性能。但是 DCL 存在一個致命缺陷载城,就是重排序?qū)е碌亩嗑€程訪問可能獲得一個未初始化的對象肌似。

首先記住上面標(biāo)記的 4 行代碼。其中第 4 行代碼?mInstance = new DCLSingleton();?在 JVM 看來有這么幾步:

為對象分配內(nèi)存空間

初始化對象

將 mInstance 引用指向第 1 步中分配的內(nèi)存地址

在單線程內(nèi)诉瓦,在不影響執(zhí)行結(jié)果的前提下川队,可能存在指令重排序。例如下列代碼:

inta?=1;

intb?=2;

在 JVM 中你是無法確保這兩行代碼誰先執(zhí)行的垦搬,因為誰先執(zhí)行都不影響程序運行結(jié)果呼寸。同理艳汽,創(chuàng)建實例對象的三部中猴贰,第 2 步?初始化對象和 第 3 步將 mInstance 引用指向?qū)ο蟮膬?nèi)存地址?之間也是可能存在重排序的。

為對象分配內(nèi)存空間

將 mInstance 引用指向第 1 步中分配的內(nèi)存地址

初始化對象

這樣的話河狐,就存在這樣一種可能米绕。線程 A 按上面重排序之后的指令執(zhí)行,當(dāng)執(zhí)行到第 2 行?將 mInstance 引用指向?qū)ο蟮膬?nèi)存地址時馋艺,線程 B 開始執(zhí)行了栅干,此時線程 A 已為mInstance賦值,線程 B 進(jìn)行 DCL 的第一次判斷if (mInstance == null),結(jié)果為false捐祠,直接返回mInstance?指向的對象碱鳞,但是由于重排序的緣故,對象其實尚未初始化踱蛀,這樣就出問題了窿给。還挺繞口的,借用 《Java 并發(fā)編程藝術(shù)》 中的一張表格率拒,會對執(zhí)行流程更加清晰崩泡。

時間線程 A線程 B

t1A1: 分配對象的內(nèi)存空間

t2A3: 設(shè)置 mInstance 指向內(nèi)存空間

t3B1: 判斷 mInstance 是否為空

t4B2: 由于 mInstance 不為空,線程 B 將訪問 mInstance 指向的對象

t5A2: 初始化對象

t6A3: 訪問 mInstance 引用的對象

A3和A2?發(fā)生重排序?qū)е戮€程 B 獲取了一個尚未初始化的對象猬膨。

說了半天角撞,該怎么改?其實很簡單,禁止多線程下的重排序就可以了谒所,只需要用?volatile關(guān)鍵字修飾mInstance?热康。在 JDK 1.5 中,增強了 volatile 的內(nèi)存語義百炬,對一個volatile 域的寫褐隆,happens-before 于任意后續(xù)對這個 volatile 域的讀。volatile 會禁止一些處理器重排序剖踊,此時 DCL 就做到了真正的線程安全庶弃。

4、靜態(tài)內(nèi)部類模式

publicclassStaticInnerSingleton{

privateStaticInnerSingleton(){}

privatestaticclassSingletonHolder{

privatestaticfinalStaticInnerSingleton?mInstance=newStaticInnerSingleton();

}

publicstaticStaticInnerSingletongetInstance(){

returnSingletonHolder.mInstance;

}

}

鑒于 DCL 繁瑣的代碼德澈,程序員又發(fā)明了靜態(tài)內(nèi)部類模式歇攻,它和餓漢式一樣基于類加載時器的線程安全,但是又做到了延遲加載梆造。SingletonHolder?是一個靜態(tài)內(nèi)部類缴守,當(dāng)外部類被加載的時候并不會初始化。當(dāng)調(diào)用?getInstance()?方法時镇辉,才會被加載屡穗。

枚舉單例暫且不提,放在最后再說忽肛。先對上面的單例模式做個檢測村砂。

二、真的是單例屹逛?

還記得開頭的提問嗎础废?

你的單例線程安全嗎?

你的單例反射安全嗎?

你的單例序列化安全嗎罕模?

上面大篇幅的論述都在說明線程安全评腺。下面看看反射安全和序列化安全。

1淑掌、反射安全

直接上代碼蒿讥,我用 DCL 來做測試:

publicstaticvoidmain(String[]?args){

DCLSingleton?singleton1?=?DCLSingleton.getInstance();

DCLSingleton?singleton2?=null;

try{

Class?clazz?=?DCLSingleton.class;

Constructor?constructor?=?clazz.getDeclaredConstructor();

constructor.setAccessible(true);

singleton2?=?constructor.newInstance();

}catch(Exception?e)?{

e.printStackTrace();

}

System.out.println(singleton1.hashCode());

System.out.println(singleton2.hashCode());

}

執(zhí)行結(jié)果:

1627674070

1360875712

很無情,通過反射破壞了單例抛腕。如何保證反射安全呢芋绸?只能以暴制暴,當(dāng)已經(jīng)存在實例的時候再去調(diào)用構(gòu)造函數(shù)直接拋出異常兽埃,對構(gòu)造函數(shù)做如下修改:

privateDCLSingleton(){

if(mInstance!=null)

thrownewRuntimeException("想反射我侥钳,沒門!");

}

上面的測試代碼會直接拋出異常柄错。

2舷夺、序列化安全

將你的單例類實現(xiàn)Serializable?持久化保存起來苦酱,日后再恢復(fù)出來,他還是單例嗎给猾?

publicstaticvoidmain(String[]?args){

DCLSingleton?singleton1?=?DCLSingleton.getInstance();

DCLSingleton?singleton2?=null;

try{

ObjectOutput?output=newObjectOutputStream(newFileOutputStream("singleton.ser"));

output.writeObject(singleton1);

output.close();

ObjectInput?input=newObjectInputStream(newFileInputStream("singleton.ser"));

singleton2=?(DCLSingleton)?input.readObject();

}catch(Exception?e)?{

e.printStackTrace();

}

System.out.println(singleton1.hashCode());

System.out.println(singleton2.hashCode());

}

執(zhí)行結(jié)果:

644117698

793589513

不堪一擊疫萤。反序列化時生成了新的實例對象。要修復(fù)也很簡單敢伸,只需要修改反序列化的邏輯就可以了扯饶,即重寫?readResolve()?方法,使其返回統(tǒng)一實例池颈。

protectedObjectreadResolve(){

returngetInstance();

}

脆弱不堪的單例模式經(jīng)過重重考驗尾序,進(jìn)化成了完全體,延遲加載躯砰,線程安全每币,反射安全,序列化安全琢歇。全部代碼如下:

publicclassDCLSingletonimplementsSerializable{

privatestaticDCLSingleton?mInstance;

privateDCLSingleton(){

if(mInstance!=null)

thrownewRuntimeException("想反射我兰怠,沒門!");

}

publicstaticDCLSingletongetInstance(){

if(mInstance?==null)?{

synchronized(DCLSingleton.class)?{

if(mInstance?==null)

mInstance?=newDCLSingleton();

}

}

returnmInstance;

}

protectedObjectreadResolve(){

returngetInstance();

}

}

三李茫、枚舉單例

枚舉看到 DCL 就開始嘲笑他了揭保,“你瞅瞅你那是啥,寫個單例費那大勁呢魄宏?” 于是擼起袖子自己寫了一個枚舉單例:

publicenumEnumSingleton?{

INSTANCE;

}

DCL 反問秸侣,“你這啥玩意凶伙,你這就是單例了?我來扒了你的皮看看 蜻拨!” 于是 DCL 掏出 jad 妄辩,扒了 Enum 的衣服,拉出來示眾:

publicfinalclassEnumSingletonextendsEnum{

publicstaticEnumSingleton[]?values()?{

return(EnumSingleton[])$VALUES.clone();

}

publicstaticEnumSingletonvalueOf(String?s){

return(EnumSingleton)Enum.valueOf(test/singleton/EnumSingleton,?s);

}

privateEnumSingleton(String?s,inti){

super(s,?i);

}

publicstaticfinalEnumSingleton?INSTANCE;

privatestaticfinalEnumSingleton?$VALUES[];

static{

INSTANCE?=newEnumSingleton("INSTANCE",0);

$VALUES?=?(newEnumSingleton[]?{

INSTANCE

});

}

}

我們依次來檢查枚舉單例的線程安全然想,反射安全,序列化安全。

首先枚舉單例無疑是線程安全的匕得,類似餓漢式,INSTANCE?的初始化放在了 static 靜態(tài)代碼段中巾表,在類加載階段執(zhí)行汁掠。由此可見,枚舉單例并不是延時加載的集币。

對于反射安全考阱,又要掏出上面的檢測代碼了,根據(jù)EnumSingleton?的構(gòu)造器鞠苟,需要稍微做些改動:

publicstaticvoidmain(String[]?args){

EnumSingleton?singleton1?=?EnumSingleton.INSTANCE;

EnumSingleton?singleton2?=null;

try{

Class?clazz?=?EnumSingleton.class;

Constructor?constructor?=?clazz.getDeclaredConstructor(String.class,int.class);

constructor.setAccessible(true);

singleton2?=?constructor.newInstance("test",1);

}catch(Exception?e)?{

e.printStackTrace();

}

System.out.println(singleton1.hashCode());

System.out.println(singleton2.hashCode());

}

結(jié)果直接報錯乞榨,錯誤日志如下:

java.lang.IllegalArgumentException:Cannotreflectivelycreateenumobjects

atjava.lang.reflect.Constructor.newInstance(Constructor.java:417)

atsingleton.SingleTest.main(SingleTest.java:16)

錯誤發(fā)生在Constructor.newInstance()方法秽之,又要從源碼中找答案了,在newInstance()?源碼中吃既,有這么一句:

if((clazz.getModifiers()?&?Modifier.ENUM)?!=0)

thrownewIllegalArgumentException("Cannot?reflectively?create?enum?objects");

如果是枚舉修飾的考榨,直接拋出異常。和之前的對抗反射的手段一致鹦倚,壓根就不給你反射河质。所以,枚舉單例也是天生反射安全的震叙。

最后枚舉單例也是序列化安全的掀鹅,上篇文章中已經(jīng)說明過,你可以運行測試代碼試試媒楼。

看起來枚舉單例的確是個不錯的選擇淫半,代碼簡單,又能保證絕大多數(shù)情況下的單例實例唯一匣砖。但是真正在開發(fā)中大家好像用的并不多科吭,更多的可能應(yīng)該是枚舉在 Java 1.5 中才添加,大家默認(rèn)已經(jīng)習(xí)慣了其他的單例實現(xiàn)方式猴鲫。

四对人、代碼最少的單例?

說到枚舉單例代碼簡單拂共,Kotlin 第一個站出來不服了牺弄。我敢說第一,誰敢說第二宜狐,給你們獻(xiàn)丑了:

object?KotlinSingleton?{?}

jad 反編譯一下:

publicfinalclassKotlinSingleton{

privateKotlinSingleton(){

}

publicstaticfinalKotlinSingleton?INSTANCE;

static{

KotlinSingleton?kotlinsingleton?=newKotlinSingleton();

INSTANCE?=?kotlinsingleton;

}

}

可以看到势告,Kotlin 的單例其實也是餓漢式的一種,不鉆牛角尖的話抚恒,基本可以滿足大部分需求咱台。

吹毛求疵的談了談單例模式,可以看見要完全的保證單例還是有很多坑點的俭驮。在開發(fā)中并沒有必要鉆牛角尖回溺,例如 Kotlin 默認(rèn)提供的單例實現(xiàn)就是餓漢式而已,其實已經(jīng)可以滿足絕大多數(shù)的情況了混萝。

由枚舉引申出了這么一篇文章遗遵,大家姑且可以當(dāng)做娛樂看一看,交個朋友逸嘀。


關(guān)注公眾號獲取更多java資源
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末车要,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子崭倘,更是在濱河造成了極大的恐慌翼岁,老刑警劉巖维哈,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異登澜,居然都是意外死亡阔挠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門脑蠕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來购撼,“玉大人,你說我怎么就攤上這事谴仙∮厍螅” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵晃跺,是天一觀的道長揩局。 經(jīng)常有香客問我,道長掀虎,這世上最難降的妖魔是什么凌盯? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮烹玉,結(jié)果婚禮上驰怎,老公的妹妹穿的比我還像新娘。我一直安慰自己二打,他們只是感情好县忌,可當(dāng)我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著继效,像睡著了一般症杏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瑞信,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天厉颤,我揣著相機(jī)與錄音,去河邊找鬼喧伞。 笑死走芋,一個胖子當(dāng)著我的面吹牛绩郎,可吹牛的內(nèi)容都是我干的潘鲫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼肋杖,長吁一口氣:“原來是場噩夢啊……” “哼溉仑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起状植,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤浊竟,失蹤者是張志新(化名)和其女友劉穎怨喘,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體振定,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡必怜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了后频。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梳庆。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖卑惜,靈堂內(nèi)的尸體忽然破棺而出膏执,到底是詐尸還是另有隱情,我是刑警寧澤露久,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布更米,位于F島的核電站,受9級特大地震影響毫痕,放射性物質(zhì)發(fā)生泄漏征峦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一消请、第九天 我趴在偏房一處隱蔽的房頂上張望眶痰。 院中可真熱鬧,春花似錦梯啤、人聲如沸竖伯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽七婴。三九已至,卻和暖如春察滑,著一層夾襖步出監(jiān)牢的瞬間打厘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工贺辰, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留户盯,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓饲化,卻偏偏與公主長得像莽鸭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子吃靠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,585評論 2 359

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