我向面试官讲解了单例模式,他对我竖起了大拇指
作者:小菠蘿
單例模式相信大家都有所聽(tīng)聞,甚至也寫(xiě)過(guò)不少了,在面試中也是考得最多的其中一個(gè)設(shè)計(jì)模式,面試官常常會(huì)要求寫(xiě)出兩種類型的單例模式并且解釋其原理,廢話不多說(shuō),我們開(kāi)始學(xué)習(xí)如何很好地回答這一道面試題吧。
?
什么是單例模式
面試官問(wèn)什么是單例模式時(shí),千萬(wàn)不要答非所問(wèn),給出單例模式有兩種類型之類的回答,要圍繞單例模式的定義去展開(kāi)。
單例模式是指在內(nèi)存中只會(huì)創(chuàng)建且僅創(chuàng)建一次對(duì)象的設(shè)計(jì)模式。在程序中多次使用同一個(gè)對(duì)象且作用相同時(shí),為了防止頻繁地創(chuàng)建對(duì)象使得內(nèi)存飆升,單例模式可以讓程序僅在內(nèi)存中創(chuàng)建一個(gè)對(duì)象,讓所有需要調(diào)用的地方都共享這一單例對(duì)象。
?
單例模式的類型
單例模式有兩種類型:
懶漢式:在真正需要使用對(duì)象時(shí)才去創(chuàng)建該單例類對(duì)象
餓漢式:在類加載時(shí)已經(jīng)創(chuàng)建好該單例對(duì)象,等待被程序使用
懶漢式創(chuàng)建單例對(duì)象
懶漢式創(chuàng)建對(duì)象的方法是在程序使用對(duì)象前,先判斷該對(duì)象是否已經(jīng)實(shí)例化(判空),若已實(shí)例化直接返回該類對(duì)象。否則則先執(zhí)行實(shí)例化操作。
根據(jù)上面的流程圖,就可以寫(xiě)出下面的這段代碼
public?class?Singleton?{private?static?Singleton?singleton;private?Singleton(){}public?static?Singleton?getInstance()?{if?(singleton?==?null)?{singleton?=?new?Singleton();}return?singleton;}}沒(méi)錯(cuò),這里我們已經(jīng)寫(xiě)出了一個(gè)很不錯(cuò)的單例模式,不過(guò)它不是完美的,但是這并不影響我們使用這個(gè)“單例對(duì)象”。
以上就是懶漢式創(chuàng)建單例對(duì)象的方法,我會(huì)在后面解釋這段代碼在哪里可以優(yōu)化,存在什么問(wèn)題。
餓漢式創(chuàng)建單例對(duì)象
餓漢式在類加載時(shí)已經(jīng)創(chuàng)建好該對(duì)象,在程序調(diào)用時(shí)直接返回該單例對(duì)象即可,即我們?cè)诰幋a時(shí)就已經(jīng)指明了要馬上創(chuàng)建這個(gè)對(duì)象,不需要等到被調(diào)用時(shí)再去創(chuàng)建。
關(guān)于類加載,涉及到JVM的內(nèi)容,我們目前可以簡(jiǎn)單認(rèn)為在程序啟動(dòng)時(shí),這個(gè)單例對(duì)象就已經(jīng)創(chuàng)建好了。
public?class?Singleton{private?static?final?Singleton?singleton?=?new?Singleton();private?Singleton(){}public?static?Singleton?getInstance()?{return?singleton;} }注意上面的代碼在第3行已經(jīng)實(shí)例化好了一個(gè)Singleton對(duì)象在內(nèi)存中,不會(huì)有多個(gè)Singleton對(duì)象實(shí)例存在
類在加載時(shí)會(huì)在堆內(nèi)存中創(chuàng)建一個(gè)Singleton對(duì)象,當(dāng)類被卸載時(shí),Singleton對(duì)象也隨之消亡了。
? ?
懶漢式如何保證只創(chuàng)建一個(gè)對(duì)象
我們?cè)賮?lái)回顧懶漢式的核心方法
public?static?Singleton?getInstance()?{if?(singleton?==?null)?{singleton?=?new?Singleton();}return?singleton; }這個(gè)方法其實(shí)是存在問(wèn)題的,試想一下,如果兩個(gè)線程同時(shí)判斷 singleton 為空,那么它們都會(huì)去實(shí)例化一個(gè)Singleton 對(duì)象,這就變成多例了。所以,我們要解決的是線程安全問(wèn)題。
最容易想到的解決方法就是在方法上加鎖,或者是對(duì)類對(duì)象加鎖,程序就會(huì)變成下面這個(gè)樣子
public?static?synchronized?Singleton?getInstance()?{if?(singleton?==?null)?{singleton?=?new?Singleton();}return?singleton; } //?或者 public?static?Singleton?getInstance()?{synchronized(Singleton.class)?{???if?(singleton?==?null)?{singleton?=?new?Singleton();}}return?singleton; }這樣就規(guī)避了兩個(gè)線程同時(shí)創(chuàng)建Singleton對(duì)象的風(fēng)險(xiǎn),但是引來(lái)另外一個(gè)問(wèn)題:每次去獲取對(duì)象都需要先獲取鎖,并發(fā)性能非常地差,極端情況下,可能會(huì)出現(xiàn)卡頓現(xiàn)象。接下來(lái)要做的就是優(yōu)化性能:目標(biāo)是如果沒(méi)有實(shí)例化對(duì)象則加鎖創(chuàng)建,如果已經(jīng)實(shí)例化了,則不需要加鎖,直接獲取實(shí)例
所以直接在方法上加鎖的方式就被廢掉了,因?yàn)檫@種方式無(wú)論如何都需要先獲取鎖
public?static?Singleton?getInstance()?{if?(singleton?==?null)?{??//?線程A和線程B同時(shí)看到singleton?=?null,如果不為null,則直接返回singletonsynchronized(Singleton.class)?{?//?線程A或線程B獲得該鎖進(jìn)行初始化if?(singleton?==?null)?{?//?其中一個(gè)線程進(jìn)入該分支,另外一個(gè)線程則不會(huì)進(jìn)入該分支singleton?=?new?Singleton();}}}return?singleton; }上面的代碼已經(jīng)完美地解決了并發(fā)安全 + 性能低效問(wèn)題:
第 2 行代碼,如果 singleton 不為空,則直接返回對(duì)象,不需要獲取鎖;而如果多個(gè)線程發(fā)現(xiàn) singleton 為空,則進(jìn)入分支;
第 3 行代碼,多個(gè)線程嘗試爭(zhēng)搶同一個(gè)鎖,只有一個(gè)線程爭(zhēng)搶成功,第一個(gè)獲取到鎖的線程會(huì)再次判斷singleton 是否為空,因?yàn)?singleton 有可能已經(jīng)被之前的線程實(shí)例化
其它之后獲取到鎖的線程在執(zhí)行到第 4 行校驗(yàn)代碼,發(fā)現(xiàn) singleton 已經(jīng)不為空了,則不會(huì)再 new 一個(gè)對(duì)象,直接返回對(duì)象即可
之后所有進(jìn)入該方法的線程都不會(huì)去獲取鎖,在第一次判斷 singleton 對(duì)象時(shí)已經(jīng)不為空了
因?yàn)樾枰獌纱闻锌?#xff0c;且對(duì)類對(duì)象加鎖,該懶漢式寫(xiě)法也被稱為:Double Check(雙重校驗(yàn)) + Lock(加鎖)
完整的代碼如下所示:
public?class?Singleton?{private?static?Singleton?singleton;private?Singleton(){}public?static?Singleton?getInstance()?{if?(singleton?==?null)?{??//?線程A和線程B同時(shí)看到singleton?=?null,如果不為null,則直接返回singletonsynchronized(Singleton.class)?{?//?線程A或線程B獲得該鎖進(jìn)行初始化if?(singleton?==?null)?{?//?其中一個(gè)線程進(jìn)入該分支,另外一個(gè)線程則不會(huì)進(jìn)入該分支singleton?=?new?Singleton();}}}return?singleton;}}上面這段代碼已經(jīng)近似完美了,但是還存在最后一個(gè)問(wèn)題:指令重排
?
使用 volatile 防止指令重排
創(chuàng)建一個(gè)對(duì)象,在 JVM 中會(huì)經(jīng)過(guò)三步:
(1)為 singleton 分配內(nèi)存空間
(2)初始化 singleton 對(duì)象
(3)將 singleton 指向分配好的內(nèi)存空間
指令重排序是指:JVM 在保證最終結(jié)果正確的情況下,可以不按照程序編碼的順序執(zhí)行語(yǔ)句,盡可能提高程序的性能
在這三步中,第 2、3 步有可能會(huì)發(fā)生指令重排現(xiàn)象,創(chuàng)建對(duì)象的順序變?yōu)?1-3-2,會(huì)導(dǎo)致多個(gè)線程獲取對(duì)象時(shí),有可能線程 A 創(chuàng)建對(duì)象的過(guò)程中,執(zhí)行了 1、3 步驟,線程 B 判斷 singleton 已經(jīng)不為空,獲取到未初始化的singleton 對(duì)象,就會(huì)報(bào) NPE 異常。文字較為晦澀,可以看流程圖:
使用 volatile 關(guān)鍵字可以防止指令重排序,其原理較為復(fù)雜,這篇文章不打算展開(kāi),可以這樣理解:使用 volatile 關(guān)鍵字修飾的變量,可以保證其指令執(zhí)行的順序與程序指明的順序一致,不會(huì)發(fā)生順序變換,這樣在多線程環(huán)境下就不會(huì)發(fā)生 NPE 異常了。
volatile 還有第二個(gè)作用:使用 volatile 關(guān)鍵字修飾的變量,可以保證其內(nèi)存可見(jiàn)性,即每一時(shí)刻線程讀取到該變量的值都是內(nèi)存中最新的那個(gè)值,線程每次操作該變量都需要先讀取該變量。
最終的代碼如下所示:
public?class?Singleton?{private?static?volatile?Singleton?singleton;private?Singleton(){}public?static?Singleton?getInstance()?{if?(singleton?==?null)?{??//?線程A和線程B同時(shí)看到singleton?=?null,如果不為null,則直接返回singletonsynchronized(Singleton.class)?{?//?線程A或線程B獲得該鎖進(jìn)行初始化if?(singleton?==?null)?{?//?其中一個(gè)線程進(jìn)入該分支,另外一個(gè)線程則不會(huì)進(jìn)入該分支singleton?=?new?Singleton();}}}return?singleton;}}?
破壞懶漢式單例與餓漢式單例
無(wú)論是完美的懶漢式還是餓漢式,終究敵不過(guò)反射和序列化,它們倆都可以把單例對(duì)象破壞掉(產(chǎn)生多個(gè)對(duì)象)。
利用反射破壞單例模式
下面是一段使用反射破壞單例模式的例子
public?static?void?main(String[]?args)?{//?獲取類的顯式構(gòu)造器Constructor<Singleton>?construct?=?Singleton.class.getDeclaredConstructor();//?可訪問(wèn)私有構(gòu)造器construct.setAccessible(true);?//?利用反射構(gòu)造新對(duì)象Singleton?obj1?=?construct.newInstance();?//?通過(guò)正常方式獲取單例對(duì)象Singleton?obj2?=?Singleton.getInstance();?System.out.println(obj1?==?obj2);?//?false }上述的代碼一針見(jiàn)血了:利用反射,強(qiáng)制訪問(wèn)類的私有構(gòu)造器,去創(chuàng)建另一個(gè)對(duì)象
利用序列化與反序列化破壞單例模式
下面是一種使用序列化和反序列化破壞單例模式的例子
public?static?void?main(String[]?args)?{//?創(chuàng)建輸出流ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("Singleton.file"));//?將單例對(duì)象寫(xiě)到文件中oos.writeObject(Singleton.getInstance());//?從文件中讀取單例對(duì)象File?file?=?new?File("Singleton.file");ObjectInputStream?ois?=??new?ObjectInputStream(new?FileInputStream(file));Singleton?newInstance?=?(Singleton)?ois.readObject();//?判斷是否是同一個(gè)對(duì)象System.out.println(newInstance?==?Singleton.getInstance());?//?false }兩個(gè)對(duì)象地址不相等的原因是:readObject() 方法讀入對(duì)象時(shí)它必定會(huì)返回一個(gè)新的對(duì)象實(shí)例,必然指向新的內(nèi)存地址。
讓面試官鼓掌的枚舉實(shí)現(xiàn)
我們已經(jīng)掌握了懶漢式與餓漢式的常見(jiàn)寫(xiě)法了,通常情況下到這里已經(jīng)足夠了。但是,追求極致的我們,怎么能夠止步于此,在《Effective Java》書(shū)中,給出了終極解決方法,話不多說(shuō),學(xué)完下面,真的不虛面試官考你了。
在 JDK 1.5 后,使用 Java 語(yǔ)言實(shí)現(xiàn)單例模式的方式又多了一種:枚舉
枚舉實(shí)現(xiàn)單例模式完整代碼如下:
public?enum?Singleton?{INSTANCE;public?void?doSomething()?{System.out.println("這是枚舉類型的單例模式!");} }使用枚舉實(shí)現(xiàn)單例模式較其它兩種實(shí)現(xiàn)方式的優(yōu)勢(shì)有 3 點(diǎn),讓我們來(lái)細(xì)品。
優(yōu)勢(shì) 1 :一目了然的代碼
代碼對(duì)比餓漢式與懶漢式來(lái)說(shuō),更加地簡(jiǎn)潔。最少只需要3行代碼,就可以完成一個(gè)單例模式:
public?enum?Test?{INSTANCE; }我們從最直觀的地方入手,第一眼看到這3行代碼,就會(huì)感覺(jué)到少,沒(méi)錯(cuò),就是少,雖然這優(yōu)勢(shì)有些牽強(qiáng),但寫(xiě)的代碼越少,越不容易出錯(cuò)。
優(yōu)勢(shì) 2:天然的線程安全與單一實(shí)例
它不需要做任何額外的操作,就可以保證對(duì)象單一性與線程安全性。
我寫(xiě)了一段測(cè)試代碼放在下面,這一段代碼可以證明程序啟動(dòng)時(shí)僅會(huì)創(chuàng)建一個(gè) Singleton 對(duì)象,且是線程安全的。
我們可以簡(jiǎn)單地理解枚舉創(chuàng)建實(shí)例的過(guò)程:在程序啟動(dòng)時(shí),會(huì)調(diào)用 Singleton 的空參構(gòu)造器,實(shí)例化好一個(gè)Singleton 對(duì)象賦給 INSTANCE,之后再也不會(huì)實(shí)例化
public?enum?Singleton?{INSTANCE;Singleton()?{?System.out.println("枚舉創(chuàng)建對(duì)象了");?}public?static?void?main(String[]?args)?{?/*?test();?*/?}public?void?test()?{Singleton?t1?=?Singleton.INSTANCE;Singleton?t2?=?Singleton.INSTANCE;System.out.print("t1和t2的地址是否相同:"?+?t1?==?t2);} } //?枚舉創(chuàng)建對(duì)象了 // t1和t2的地址是否相同:true除了優(yōu)勢(shì)1和優(yōu)勢(shì)2,還有最后一個(gè)優(yōu)勢(shì)是 保護(hù)單例模式,它使得枚舉在當(dāng)前的單例模式領(lǐng)域已經(jīng)是 無(wú)懈可擊 了
優(yōu)勢(shì) 3:枚舉保護(hù)單例模式不被破壞
使用枚舉可以防止調(diào)用者使用反射、序列化與反序列化機(jī)制強(qiáng)制生成多個(gè)單例對(duì)象,破壞單例模式。
防反射
枚舉類默認(rèn)繼承了 Enum 類,在利用反射調(diào)用 newInstance() 時(shí),會(huì)判斷該類是否是一個(gè)枚舉類,如果是,則拋出異常。
防止反序列化創(chuàng)建多個(gè)枚舉對(duì)象
在讀入 Singleton 對(duì)象時(shí),每個(gè)枚舉類型和枚舉名字都是唯一的,所以在序列化時(shí),僅僅只是對(duì)枚舉的類型和變量名輸出到文件中,在讀入文件反序列化成對(duì)象時(shí),使用 Enum 類的 valueOf(String name) 方法根據(jù)變量的名字查找對(duì)應(yīng)的枚舉對(duì)象。
所以,在序列化和反序列化的過(guò)程中,只是寫(xiě)出和讀入了枚舉類型和名字,沒(méi)有任何關(guān)于對(duì)象的操作。
小結(jié):
(1)Enum 類內(nèi)部使用Enum 類型判定防止通過(guò)反射創(chuàng)建多個(gè)對(duì)象
(2)Enum 類通過(guò)寫(xiě)出(讀入)對(duì)象類型和枚舉名字將對(duì)象序列化(反序列化),通過(guò) valueOf() 方法匹配枚舉名找到內(nèi)存中的唯一的對(duì)象實(shí)例,防止通過(guò)反序列化構(gòu)造多個(gè)對(duì)象
(3)枚舉類不需要關(guān)注線程安全、破壞單例和性能問(wèn)題,因?yàn)槠鋭?chuàng)建對(duì)象的時(shí)機(jī)與餓漢式單例有異曲同工之妙。
?
總結(jié)
(1)單例模式常見(jiàn)的寫(xiě)法有兩種:懶漢式、餓漢式
(2)懶漢式:在需要用到對(duì)象時(shí)才實(shí)例化對(duì)象,正確的實(shí)現(xiàn)方式是:Double Check + Lock,解決了并發(fā)安全和性能低下問(wèn)題
(3)餓漢式:在類加載時(shí)已經(jīng)創(chuàng)建好該單例對(duì)象,在獲取單例對(duì)象時(shí)直接返回對(duì)象即可,不會(huì)存在并發(fā)安全和性能問(wèn)題。
(4)在開(kāi)發(fā)中如果對(duì)內(nèi)存要求非常高,那么使用懶漢式寫(xiě)法,可以在特定時(shí)候才創(chuàng)建該對(duì)象;
(5)如果對(duì)內(nèi)存要求不高使用餓漢式寫(xiě)法,因?yàn)楹?jiǎn)單不易出錯(cuò),且沒(méi)有任何并發(fā)安全和性能問(wèn)題
(6)為了防止多線程環(huán)境下,因?yàn)橹噶钪嘏判驅(qū)е伦兞繄?bào)NPE,需要在單例對(duì)象上添加 volatile 關(guān)鍵字防止指令重排序
(7)最優(yōu)雅的實(shí)現(xiàn)方式是使用枚舉,其代碼精簡(jiǎn),沒(méi)有線程安全問(wèn)題,且 Enum 類內(nèi)部防止反射和反序列化時(shí)破壞單例。
有道無(wú)術(shù),術(shù)可成;有術(shù)無(wú)道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
新人創(chuàng)作打卡挑戰(zhàn)賽發(fā)博客就能抽獎(jiǎng)!定制產(chǎn)品紅包拿不停!總結(jié)
以上是生活随笔為你收集整理的我向面试官讲解了单例模式,他对我竖起了大拇指的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: keil5一点project就闪退
- 下一篇: 用 Python 分析了 20 万场吃鸡