例模式是面向?qū)ο蟮木幊陶Z(yǔ)言23種設(shè)計(jì)模式之一注暗,屬于創(chuàng)建型設(shè)計(jì)模式。主要用于解決對(duì)象的頻繁創(chuàng)建與銷毀問(wèn)題墓猎,因?yàn)閱卫J奖WC一個(gè)類僅會(huì)有一個(gè)實(shí)例捆昏。大部分對(duì)單例模式應(yīng)該都知道一些,但面試的時(shí)候可能回答不會(huì)很完整陶衅,不能給自己加分屡立,甚至扣分。
單一的知識(shí)點(diǎn)并不能對(duì)自己在面試的時(shí)候帶來(lái)加分,而系統(tǒng)的知識(shí)樹(shù)則會(huì)讓面試官另眼相看膨俐,而本文會(huì)系統(tǒng)的介紹單例模式的基礎(chǔ)版本與完美版本勇皇,基本上將單例模式的內(nèi)容完全包括。如果認(rèn)為有不同的意見(jiàn)可以留言交流焚刺。
單例模式最重要的就是保證一個(gè)類只會(huì)出現(xiàn)一個(gè)實(shí)例敛摘,那么超過(guò)一個(gè)就不能被稱為是單例,所有其代碼構(gòu)成如下特點(diǎn)乳愉。
私有化構(gòu)造器兄淫,禁止從外部創(chuàng)建單例對(duì)象。
提供一個(gè)全局的訪問(wèn)點(diǎn)獲取單例對(duì)象蔓姚。
什么是全局訪問(wèn)點(diǎn)捕虽? 好吧,上面的話語(yǔ)太文鄒鄒了坡脐,如果我說(shuō)公共的靜態(tài)方法呢泄私?
主要分為餓漢模式和懶漢模式备闲。那何為餓漢晌端?何為懶漢?
小麗的爸爸從小生活很艱苦恬砂,經(jīng)歷了饑荒年代咧纠,所以對(duì)食物非常緊張。當(dāng)小麗去上學(xué)的時(shí)候泻骤,不管小麗是否需要漆羔,都會(huì)給小麗準(zhǔn)備很多的零食。
而小明的爸爸則是一個(gè)非常懶惰的人狱掂,所有的事情都會(huì)到最后才去做钧椰,所有事情只有當(dāng)有別人來(lái)叫他的時(shí)候,他才會(huì)把事情做完這樣就引出了我們對(duì)餓漢模式和懶漢模式的定義:
餓漢模式:不管單例對(duì)象是否被使用符欠,都會(huì)先創(chuàng)建出一個(gè)對(duì)象。餓漢模式存在資源浪費(fèi)的問(wèn)題瓶埋,因?yàn)楹苡锌赡軐?duì)象創(chuàng)建出來(lái)只會(huì)永遠(yuǎn)都不會(huì)被使用到希柿。
代碼如下:
package demo.single;
/**
* 餓漢模式
*/
publicclassHungrySingle{
/**
? ? * 餓漢模式,不管hungrySingle對(duì)象是否有使用到养筒,都會(huì)先創(chuàng)建出來(lái)
? ? * 由于餓漢模式在對(duì)象使用之前就已經(jīng)被創(chuàng)建曾撤,所以是不會(huì)存在線程安全問(wèn)題
? ? */
privatestaticHungrySingle hungrySingle =newHungrySingle();
/**
? ? * 私有化構(gòu)造器,禁止外部創(chuàng)建
? ? */
privateHungrySingle(){
? ? }
/**
? ? * 提供獲取實(shí)例的方法
? ? */
publicstaticHungrySinglegetInstance(){
returnhungrySingle;
? ? }
}
懶漢模式:不會(huì)先將對(duì)象創(chuàng)建出來(lái)晕粪,而是等到有人使用的時(shí)候才會(huì)創(chuàng)建挤悉。相比餓漢模式,懶漢模式不會(huì)存在資源浪費(fèi)的情況巫湘,所以基本都會(huì)選擇懶漢模式装悲。
代碼如下:
package demo.single;
/**
* 懶漢模式
*/
publicclassLazySingle{
/**
? ? * 懶漢模式昏鹃,不會(huì)先創(chuàng)建對(duì)象,而是在調(diào)用的時(shí)候才會(huì)創(chuàng)建對(duì)象
? ? */
privatestaticLazySingle lazySingle =null;
privateLazySingle(){
? ? }
/**
? ? * 調(diào)用的時(shí)候創(chuàng)建對(duì)象并返回
? ? */
publicstaticLazySinglegetInstance(){
if(lazySingle ==null){
lazySingle =newLazySingle();
? ? ? ? }
returnlazySingle;
? ? }
}
小李:面試官诀诊,您看我這樣的解釋可還行洞渤。
面試官:?jiǎn)尉€程下是挺好的,如果在多線程環(huán)境下呢属瓣?
小李:這個(gè)我知道载迄,加鎖啊抡蛙!
面試官:出門(mén)左轉(zhuǎn)電梯直達(dá)护昧!
其實(shí)加鎖也沒(méi)答錯(cuò),關(guān)鍵問(wèn)題在于如何加鎖粗截!
直接將獲取實(shí)例的方法內(nèi)容寫(xiě)入同步代碼塊中惋耙,解決了多線程安全的問(wèn)題,但是并發(fā)效率的問(wèn)題又暴露了出來(lái)慈格。你想啊怠晴,現(xiàn)在鎖住了這方法,而無(wú)論單例的對(duì)象是否創(chuàng)建浴捆,都會(huì)經(jīng)過(guò)獲取鎖蒜田、釋放鎖的過(guò)程。這樣的性能顯然是不能接受的选泻。
小李:我想想啊~~~冲粤! Emmmmm...! 有了,我們可以在同步代碼塊外層加一個(gè)判斷页眯,如果對(duì)象已經(jīng)創(chuàng)建則直接返回梯捕。
面試官:這樣解決了一部分的并發(fā)效率問(wèn)題,但是如果在創(chuàng)建的時(shí)候同時(shí)有很多的線程訪問(wèn)窝撵,是不是也會(huì)有并發(fā)的效率問(wèn)題呢傀顾?再優(yōu)化優(yōu)化。
小李一想碌奉,確實(shí)是這樣短曾,如果對(duì)象還沒(méi)有創(chuàng)建出來(lái)的時(shí)候,就有很多的線程來(lái)訪問(wèn)赐劣,也會(huì)出現(xiàn)問(wèn)題嫉拐,假設(shè)有兩個(gè)線程同時(shí)訪問(wèn),當(dāng)A線程優(yōu)先爭(zhēng)搶到鎖魁兼,A進(jìn)入同步代碼塊執(zhí)行婉徘,此時(shí)B沒(méi)有爭(zhēng)搶到鎖,將處于等待狀態(tài),而當(dāng)A線程執(zhí)行完成后釋放鎖盖呼,B進(jìn)入同步代碼塊執(zhí)行儒鹿,此時(shí)B線程同樣會(huì)創(chuàng)建出一個(gè)對(duì)象,破壞了單例塌计。
小李:面試官挺身,我明白了,可以在同步代碼塊中再加一層if判斷锌仅,如果對(duì)象已經(jīng)創(chuàng)建章钾,就直接返回即可。
Double Check
上面最后的結(jié)果就是我們常說(shuō)的Double Check,即雙重鎖檢查热芹。雙重鎖檢查在很多地方都被運(yùn)用到贱傀,代碼如下。
packagedemo.single;
/**
* 懶漢模式
*/
publicclassLazySingle{
/**
? ? * 懶漢模式伊脓,不會(huì)先創(chuàng)建對(duì)象府寒,而是在調(diào)用的時(shí)候才會(huì)創(chuàng)建對(duì)象
? ? */
privatestaticLazySingle lazySingle =null;
privateLazySingle() {
? ? }
/**
? ? * 調(diào)用的時(shí)候創(chuàng)建對(duì)象并返回
? ? */
publicstaticLazySingle getInstance(){
//first check
if(lazySingle ==null){
synchronized(LazySingle.class){
//doublecheck
if(lazySingle ==null){
lazySingle =newLazySingle();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
returnlazySingle;
? ? }
}
面試官:小李,你多線程運(yùn)行一下代碼看看呢报腔。
小李:好勒株搔! 好像挺正常啊。等等纯蛾, 好像不對(duì)纤房, 這里還是出現(xiàn)了多個(gè)對(duì)象!7摺炮姨!啊~~,這是為什么啊碰煌,我都懵了舒岸,這完全超出了我的能力范圍。
面試官:哈哈芦圾,小子蛾派,這下知道誰(shuí)是大佬了吧?我來(lái)給你好好解釋一下个少,其實(shí)碍脏,這和我們的代碼沒(méi)有關(guān)系,正常來(lái)講稍算,應(yīng)該不會(huì)出現(xiàn)這樣的問(wèn)題,但是我們都知道役拴,代碼在運(yùn)行過(guò)程中糊探,會(huì)被編譯成一條一條的指令運(yùn)行,而JVM在運(yùn)行時(shí),在保證單線程最終結(jié)果不會(huì)受影響的情況下科平,對(duì)指令進(jìn)行優(yōu)化褥紫,就有可能對(duì)指令進(jìn)行重排序,同樣會(huì)破壞單例瞪慧。
lazySingle=newLazySingle();
//這樣一段代碼在運(yùn)行時(shí)會(huì)生成3條指令髓考,即:1\.分配內(nèi)存空間2\.創(chuàng)建對(duì)象3\.指向引用
//正常情況下是會(huì)按照123順序執(zhí)行,但JVM優(yōu)化器進(jìn)行指令重排后弃酌,則可能變?yōu)椋?\.分配內(nèi)存空間3\.指向引用2\.創(chuàng)建對(duì)象
//在單線程下氨菇,這樣的優(yōu)化沒(méi)有問(wèn)題,但是多線程下妓湘,線程是在爭(zhēng)搶CPU時(shí)間碎片的查蓉。假設(shè)A剛剛執(zhí)行完13//條指令,此時(shí)B爭(zhēng)搶到時(shí)間碎片榜贴,發(fā)現(xiàn)對(duì)象不為空了豌研,就直接返回,但此時(shí)對(duì)象還沒(méi)有真正被創(chuàng)建唬党。B調(diào)用
//此對(duì)象就會(huì)拋出異常
//而volatile關(guān)鍵字修飾的變量可以禁止指令重排序鹃共,則可以保證指令會(huì)是123順序執(zhí)行。
//加上volatile修飾
privatevolatilestaticLazySinglelazySingle=null;
小李: 終于解決了驶拱,好難啊霜浴,一個(gè)簡(jiǎn)單的單例模式居然有這么多的細(xì)節(jié)。
面試官:你以為這就完了屯烦?
使用內(nèi)部類的方式可以非常完美的完成單例模式坷随,而實(shí)現(xiàn)代碼也非常簡(jiǎn)單。
package demo.single;
/**
* 內(nèi)部類的方式實(shí)現(xiàn)單例
*/
publicclassInnerSingle{
/**
? ? * 私有化構(gòu)造器
? ? */
privateInnerSingle(){
? ? }
/**
? ? * 私有內(nèi)部類
? ? */
privatestaticclassInner{
//Jingtai內(nèi)部類持有外部類的對(duì)象
publicstaticfinalInnerSingle SINGLE =newInnerSingle();
? ? }
/**
? ? * 返回靜態(tài)內(nèi)部類持有的對(duì)象
? ? */
publicstaticInnerSinglegetInstance(){
returnInner.SINGLE;
? ? }
}
可以看到驻龟,代碼中并沒(méi)有出現(xiàn)同步方法或者同步代碼塊温眉,那么靜態(tài)內(nèi)部類的方式是如何做到安全的單例模式呢?
外部類加載的時(shí)候翁狐,不會(huì)立即加載內(nèi)部類类溢,而是在調(diào)用的時(shí)候會(huì)加載內(nèi)部類。
不管多少線程訪問(wèn)露懒,JVM一定會(huì)保證類被正確的初始化闯冷,即靜態(tài)內(nèi)部類的方式是在JVM層面保證了線程安全
當(dāng)然,這樣也有一些缺點(diǎn)懈词,那就是在創(chuàng)建單例對(duì)象的時(shí)候蛇耀,如果需要傳參,那么靜態(tài)內(nèi)部類的方式會(huì)非常麻煩坎弯。
那么纺涤,上面的單例已經(jīng)完美了嗎译暂?并沒(méi)有,看我如何將單例給破壞掉撩炊。
反射破壞
反射可以繞過(guò)私有構(gòu)造器的限制外永,創(chuàng)建對(duì)象。當(dāng)然正常的調(diào)用是不會(huì)發(fā)生單例被破壞的情況拧咳,但是如果偏偏有人不走尋常路呢伯顶,比如下面的調(diào)用。
packagedemo.single;
importjava.lang.reflect.Constructor;
/**
* 反射破壞單例
*/
publicclassRefBreakSingleTest{
publicstatic void main(String[] args) throws Exception {
//獲取類對(duì)象
Class lazySingleClass = LazySingle.class;
? ? ? ? //獲取構(gòu)造器
Constructorconstructor= lazySingleClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
? ? ? ? //創(chuàng)建對(duì)象
LazySingle lazySingle =constructor.newInstance(null);
System.out.println(lazySingle);
System.out.println(LazySingle.getInstance());
System.out.println(lazySingle == LazySingle.getInstance());
? ? }
}
image
<figcaption>測(cè)試結(jié)果</figcaption>
很明顯看到出現(xiàn)了兩個(gè)不同的兌現(xiàn)骆膝,顯然祭衩,單例被破壞了! 對(duì)于這樣的情況該如何禁止呢谭网?在網(wǎng)上查閱了很多資料汪厨,大部分是使用變量控制法,即在類中添加一個(gè)變量用于判斷單例類的構(gòu)造器是否有被調(diào)用愉择,代碼如下劫乱。
//添加變量控制,防止反射破壞
privatestaticbooleanisInstance =false;
privatevolatilestaticLazySingle lazySingle =null;
privateLazySingle()throwsException{
if(isInstance){
thrownewException("the Constructor has be used");
? ? ? }
isInstance =true;
? }
再次調(diào)用測(cè)試代碼,發(fā)現(xiàn)不能再創(chuàng)建多個(gè)單例對(duì)象锥涕,程序拋出了異常衷戈。
image
<figcaption></figcaption>
但是別忘了,屬性也是可以通過(guò)反射修改的(count层坠、instance的判斷反射都能繞過(guò))殖妇。
publicclassRefBreakSingleTest{
publicstatic void main(String[] args) throws Exception {
//獲取類對(duì)象
Class lazySingleClass = LazySingle.class;
? ? ? ? //獲取構(gòu)造器
Constructorconstructor= lazySingleClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
? ? ? ? //創(chuàng)建對(duì)象
LazySingle lazySingle =constructor.newInstance(null);
System.out.println(lazySingle);
Field isInstance = lazySingleClass.getDeclaredField("isInstance");
isInstance.setAccessible(true);
isInstance.set(null,false);
System.out.println(LazySingle.getInstance());
System.out.println(lazySingle == LazySingle.getInstance());
? ? }
}
image
<figcaption></figcaption>
單例再次被破壞,感覺(jué)是不是已經(jīng)快崩潰了破花,一個(gè)單例咋這么多事呢Gぁ!既然私有屬性座每、私有方法在外部都能通過(guò)反射獲取前鹅,那有沒(méi)有反射不能獲取的呢?我在網(wǎng)上也找到了另外一種寫(xiě)法峭梳,即私有內(nèi)部類的來(lái)持有實(shí)例控制變量舰绘,而我也通過(guò)測(cè)試,發(fā)現(xiàn)反射同樣能夠繞過(guò)從而破壞單例葱椭。
package demo.pattren.single;
importjava.lang.reflect.Constructor;
importjava.lang.reflect.Method;
publicclassBreakInnerTest {
publicstaticvoidmain(String[] args) throws Exception {
? ? ? ? Class<LazySingle> lazySingleClass = LazySingle.class;
//? ? ? ? //獲取構(gòu)造器
Constructorconstructor= lazySingleClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
//創(chuàng)建對(duì)象
LazySingle lazySingle = constructor.newInstance(null);
//獲取內(nèi)部類的類對(duì)象
Class aClass = Class.forName("demo.pattren.single.LazySingle$InnerClass");
? ? ? ? Method[] methods = aClass.getMethods();
? ? ? ? Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
? ? ? ? System.out.println(declaredConstructors);
Constructor declaredConstructor = declaredConstructors[0];
declaredConstructor.setAccessible(true);
//創(chuàng)建內(nèi)部類需要傳入一個(gè)外部類的對(duì)象
Objecto = declaredConstructor.newInstance(lazySingle);
//成功繞過(guò)
methods[0].invoke(o);
? ? }
}
目前網(wǎng)上基本都是這兩種捂寿,但是反射都是能夠繞過(guò)判斷進(jìn)行破壞》踉耍可以這樣認(rèn)為秦陋,這種方式反射是可以破壞的,不能100%保證單例不被破壞治笨。歡迎各位提供完美的示例踱侣。
序列化破壞
Java的IO提供了對(duì)象流粪小,用來(lái)將對(duì)象寫(xiě)入磁盤(pán)、從磁盤(pán)讀取對(duì)象的功能抡句。這也成為了單例的破壞點(diǎn)。
publicstaticvoidmain(String[] args) throws Exception{
//正常的方式獲取單例對(duì)象
? ? ? ? InnerSingle instance = InnerSingle.getInstance();
//寫(xiě)入磁盤(pán)
FileOutputStream fos =newFileOutputStream("d:/single");
ObjectOutputStream oos =newObjectOutputStream(fos);
? ? ? ? oos.writeObject(instance);
? ? ? ? oos.close();
? ? ? ? fos.close();
//從磁盤(pán)讀取對(duì)象
FileInputStream fis =newFileInputStream("d:/single");
ObjectInputStream ois =newObjectInputStream(fis);
? ? ? ? InnerSingle innerSingle = (InnerSingle) ois.readObject();
System.out.println(instance);
System.out.println(innerSingle);
System.out.println(innerSingle == instance);
? ? }
image
<figcaption></figcaption>
而序列化的方式JVM提供了一種機(jī)制杠愧,可以防止單例被破壞待榔,即在單例類中添加readResovle方法。
//在反序列化時(shí)流济,readResolve方法锐锣,則直接返回該方法指定的對(duì)象
privateObject readResolve(){
returngetInstance();
? ? }
測(cè)試結(jié)果:
image
<figcaption></figcaption>
序列化沒(méi)有再破壞單例,而這一切JDK是如何處理的呢绳瘟?
publicfinalObjectreadObject()
? ? ? ? throws IOException, ClassNotFoundException
? ? {
if(enableOverride) {
returnreadObjectOverride();
? ? ? ? }
intouterHandle = passHandle;
try{
//關(guān)鍵代碼雕憔,最終返回的是此方法返回的對(duì)象
Objectobj = readObject0(false);
? ? ? ? ? ? handles.markDependency(outerHandle, passHandle);
? ? ? ? ? ? ClassNotFoundException ex = handles.lookupException(passHandle);
//more code but not importent
繼續(xù)深入,發(fā)現(xiàn)readObject0方法的關(guān)鍵代碼如下
bytetc;
//取出文件的一個(gè)字節(jié),判斷讀取的對(duì)象類型
while((tc = bin.peekByte()) == TC_RESET) {
? ? ? ? ? ? bin.readByte();
? ? ? ? ? ? handleReset();
? ? ? ? }
? ? ? ? depth++;
? ? ? ? totalObjectRefs++;
try{
switch(tc) {
caseTC_NULL:
returnreadNull();
caseTC_ENUM:
returncheckResolve(readEnum(unshared));
//判斷為對(duì)象類
caseTC_OBJECT:
returncheckResolve(readOrdinaryObject(unshared));
//more othrer case
繼續(xù)追蹤readOrdinaryObject方法糖声,發(fā)現(xiàn)readReslove的關(guān)鍵代碼
? ? ? ? //判斷是否有readReslove方法(desc.hasReadResolveMethod())
if(obj != null &&
? ? ? ? ? ? handles.lookupException(passHandle) == null &&
? ? ? ? ? ? desc.hasReadResolveMethod())
? ? ? ? {?
? ? ? ? ? //執(zhí)行readReslove
Objectrep= desc.invokeReadResolve(obj);
if(unshared &&rep.getClass().isArray()) {
rep= cloneArray(rep);
? ? ? ? ? ? }
if(rep!= obj) {
? ? ? ? ? ? ? ? // Filter the replacement object
if(rep!= null) {
if(rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
}else{
filterCheck(rep.getClass(),-1);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? //最終返回readReslove方法的執(zhí)行結(jié)果
handles.setObject(passHandle, obj =rep);
? ? ? ? ? ? }
? ? ? ? }
returnobj;
大神Josh Bloch在《Effective Java》中極力推薦使用枚舉的方式來(lái)實(shí)現(xiàn)單例斤彼。
package demo.single;
publicenumEnumSingle{
? ? SINGLE;
? ? public void doJob(){
System.out.println("doJob");
? ? }
}
枚舉類型是單例模式的最佳選擇,主要得益于JVM對(duì)于枚舉類型的支持:
JVM保證枚舉類型的每個(gè)實(shí)例僅存在一份
枚舉類型的序列化與反序列化不會(huì)破壞其單例的特性(上面的源碼大家可以去找一找)
反射也不能破壞枚舉單例
可以說(shuō)蘸泻,枚舉天然就是單例的琉苇,那么你會(huì)選擇枚舉作為單例嗎?