單例模式
定義
單例模式是一個(gè)簡(jiǎn)單的模式,定義如下:
Ensure a class has only one instance ,and provide a global point of access to it.==(確保一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例)==
下面是單例模式的通用類圖:
類圖代碼實(shí)現(xiàn):
public class Singleton {
private static final Singleton singleton = new Singleton();
// 限制產(chǎn)生多個(gè)對(duì)象
private Singleton(){}
// 通過(guò)該方法獲得實(shí)例對(duì)象
public static Singleton getSingleton() {
return singleton;
}
// 類中的其他方法,盡量是static
public static void dosomething(){}
}
public class Client {
public static void main(String args[]){
// 不能直接通過(guò)新建一個(gè)對(duì)象的方式來(lái)創(chuàng)建類的實(shí)例,只可以通過(guò)類的自身的方式創(chuàng)建對(duì)象
Singleton singleton = Singleton.getSingleton();
System.out.println(singleton.hashCode());
}
}
分析
上面的單例模式的通用代碼描述了單列模式的實(shí)現(xiàn)方式。Singleton稱為單列類,通過(guò)使用private的構(gòu)造函數(shù)確保了在一個(gè)應(yīng)用中只產(chǎn)生一個(gè)實(shí)例,并且是自實(shí)例化的。
單例模式的應(yīng)用
優(yōu)點(diǎn)
- 由于單例模式在內(nèi)存中只有一個(gè)實(shí)例,減少了內(nèi)存的開(kāi)支,特別是一個(gè)對(duì)象的頻繁的創(chuàng)建和銷毀時(shí),而且創(chuàng)建或銷毀時(shí)性能無(wú)法優(yōu)化,單例模式的優(yōu)勢(shì)就特別明顯
- 由于單例模式只生成一個(gè)實(shí)例,減少了系統(tǒng)的性能開(kāi)銷,對(duì)一個(gè)對(duì)象的產(chǎn)生需要較多的資源時(shí),如讀取配置、產(chǎn)生其他的依賴對(duì)象時(shí),則可以通過(guò)在應(yīng)用啟動(dòng)時(shí)直接產(chǎn)生一個(gè)單例對(duì)象,然后用永駐內(nèi)存的方式來(lái)解決(在JAVAEE中采用單例模式時(shí),需要注意JVM的內(nèi)存回收的機(jī)制)
- 單例模式可以避免對(duì)資源的多重占用,例如寫一個(gè)文件的動(dòng)作,由于只有一個(gè)實(shí)例在內(nèi)存中,避免對(duì)同一個(gè)資源的同時(shí)寫操作
- 單例模式可以設(shè)置全局的訪問(wèn)點(diǎn),優(yōu)化和共享資源的訪問(wèn)
缺點(diǎn)
單例模式一般沒(méi)有接口,擴(kuò)展很困難,若要擴(kuò)展,除了修改代碼基本上沒(méi)有第二種途徑可以實(shí)現(xiàn)。單例模式為什么不能增加接口呢?因?yàn)榻涌趯?duì)單例模式是沒(méi)有任何意義的,它要求“自行實(shí)例化”,并且提供單一實(shí)例、接口或抽象類是不可能被實(shí)例化的。當(dāng)然,在特殊情況下,單例模式可以實(shí)現(xiàn)接口、被繼承等,需要在系統(tǒng)開(kāi)發(fā)中根據(jù)環(huán)境判斷。
單例模式對(duì)測(cè)試是不利的。在并行開(kāi)發(fā)環(huán)境中,如果單例模式?jīng)]有完成,是不能進(jìn)行測(cè)試的,沒(méi)有接口也不能使用mock的方式虛擬一個(gè)對(duì)象。
單例模式與單一職責(zé)原則有沖突。一個(gè)類應(yīng)該只實(shí)現(xiàn)一個(gè)邏輯,而不關(guān)心它是否是單例的,是不是要單例取決于環(huán)境,單例模式把“要單例”和業(yè)務(wù)邏輯融合在一個(gè)類中。
單例模式的應(yīng)用場(chǎng)景
在一個(gè)系統(tǒng)中,要求一個(gè)類有且僅有一個(gè)對(duì)象,如果出現(xiàn)多個(gè)對(duì)象就會(huì)出現(xiàn)“不良反應(yīng)”,可以采用單例模式,具體的場(chǎng)景如下:
要求生成唯一序列號(hào)的環(huán)境;
在整個(gè)項(xiàng)目中需要一個(gè)共享訪問(wèn)點(diǎn)或共享數(shù)據(jù),例如一個(gè)Web頁(yè)面上的計(jì)數(shù)器,可以不用把每次刷新都記錄到數(shù)據(jù)庫(kù)中,使用單例模式保持計(jì)數(shù)器的值,并確保是線程安全的;
創(chuàng)建一個(gè)對(duì)象需要消耗的資源過(guò)多,如要訪問(wèn)IO和數(shù)據(jù)庫(kù)等資源;
需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境,可以采用單例模式(當(dāng)然,也可以直接聲明為static的方式)
單例模式的加載方式
懶漢模式
public class HungrySingleton {
private static HungrySingleton singleton = null;
private HungrySingleton(){}
public synchronized static HungrySingleton getSingleton(){
if(singleton == null){
singleton = new HungrySingleton();
}
return singleton;
}
}
該單例模式在低并發(fā)的情況下尚不會(huì)出現(xiàn)問(wèn)題,若系統(tǒng)壓力增大,并發(fā)量增加時(shí)則可能在內(nèi)存中出現(xiàn)多個(gè)實(shí)例,破壞了最初的預(yù)期,舉例:如一個(gè)線程A執(zhí)行到singleton=new Singleton(),但還沒(méi)有獲得對(duì)象(對(duì)象初始化是需要時(shí)間的),第二個(gè)線程B也在執(zhí)行,執(zhí)行到(singleton==null)判斷,那么線程B獲得判斷條件也是為真,于是繼續(xù)運(yùn)行下去,線程A獲得了一個(gè)對(duì)象,線程B也獲得了一個(gè)對(duì)象,在內(nèi)存中就出現(xiàn)兩個(gè)對(duì)象!
解決線程不安全的方法很有多,++可以在getSingleton方法前加synchronized關(guān)鍵字++,==也可以在getSingleton方法內(nèi)增加synchronized來(lái)實(shí)現(xiàn)==,但都不是最優(yōu)秀的單例模式,建議使用餓漢式單例,增加了synchronized的單例稱為懶漢式單例)。
餓漢模式
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getSingleton() {
return singleton;
}
}
總結(jié)
- 單例模式是中比較簡(jiǎn)單的模式,應(yīng)用也非常廣泛,如在Spring中,每個(gè)Bean默認(rèn)就是單例的,這樣做的優(yōu)點(diǎn)是Spring容器可以管理這些Bean的生命期,決定什么時(shí)候創(chuàng)建出來(lái),什么時(shí)候銷毀,銷毀的時(shí)候要如何處理,等等。如果采用非單例模式(Prototype類型),則Bean初始化后的管理交由J2EE容器,Spring容器不再跟蹤管理Bean的生命周期。
- 使用單例模式需要注意的一點(diǎn)就是JVM的垃圾回收機(jī)制,如果我們的一個(gè)單例對(duì)象在內(nèi)存中長(zhǎng)久不使用,JVM就認(rèn)為這個(gè)對(duì)象是一個(gè)垃圾,在CPU資源空閑的情況下該對(duì)象會(huì)被清理掉,下次再調(diào)用時(shí)就需要重新產(chǎn)生一個(gè)對(duì)象。如果我們?cè)趹?yīng)用中使用單例類作為有狀態(tài)值(如計(jì)數(shù)器)的管理,則會(huì)出現(xiàn)恢復(fù)原狀的情況,應(yīng)用就會(huì)出現(xiàn)故障。如果確實(shí)需要采用單例模式來(lái)記錄有狀態(tài)的值,有兩種辦法可以解決該問(wèn)題:
i.由容器管理單例的生命周期
Java EE容器或者框架級(jí)容器(如Spring)可以讓對(duì)象長(zhǎng)久駐留內(nèi)存。當(dāng)然,自行通過(guò)管理對(duì)象的生命期也是一個(gè)可行的辦法,既然有那么多的工具提供給我們,為什么不用呢?
ii. 狀態(tài)隨時(shí)記錄
可以使用異步記錄的方式,或者使用觀察者模式,記錄狀態(tài)的變化,寫入文件或?qū)懭霐?shù)據(jù)庫(kù)中,確保即使單例對(duì)象重新初始化也可以從資源環(huán)境獲得銷毀前的數(shù)據(jù),避免應(yīng)用數(shù)據(jù)丟失。