1 單例模式的定義
定義:確保某一個(gè)類只有一個(gè)實(shí)例,自行實(shí)例化并且向整個(gè)系統(tǒng)提供這個(gè)實(shí)例淮蜈。
單例模式的通用類圖如下所示:
Singleton稱為單例類,通過(guò)使用private的構(gòu)造函數(shù)確保了在一個(gè)應(yīng)用中只產(chǎn)生一個(gè)實(shí)例(應(yīng)用啟動(dòng)的時(shí)候,自行實(shí)例化)浩村。
2 單例模式的優(yōu)點(diǎn)和缺點(diǎn)
單例模式的優(yōu)點(diǎn):
- 提高效率
當(dāng)一個(gè)對(duì)象需要頻繁的創(chuàng)建和銷毀,并且對(duì)象的創(chuàng)建和銷毀操作性能無(wú)法優(yōu)化护蝶。此時(shí)华烟,單例模式可以在應(yīng)用啟動(dòng)的時(shí)候就產(chǎn)生一個(gè)實(shí)例,永久駐留內(nèi)存持灰,可以減少系統(tǒng)創(chuàng)建和銷毀實(shí)例的性能開(kāi)銷盔夜,非常明顯地提高效率。另外堤魁,單例模式在內(nèi)存中只有一個(gè)實(shí)例喂链,可以減少內(nèi)存開(kāi)支。比如妥泉,讀取配置等椭微。 - 避免對(duì)資源的多重占用
例如一個(gè)對(duì)文件的寫(xiě)操作,由于只有一個(gè)實(shí)例盲链,避免對(duì)同一個(gè)資源文件同時(shí)寫(xiě)蝇率。 - 在系統(tǒng)設(shè)置全局訪問(wèn)點(diǎn),優(yōu)化和共享資源訪問(wèn)刽沾。
單例模式的缺點(diǎn):
- 單例模式一般沒(méi)有接口本慕,擴(kuò)展困難。
- 單例測(cè)試對(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)心是否是單例的。單例模式把單例和業(yè)務(wù)邏輯融合在一個(gè)類中骡男。
3 單例模式的應(yīng)用場(chǎng)景
在一個(gè)系統(tǒng)中淆游,要求一個(gè)類有且僅有一個(gè)實(shí)例,如果出現(xiàn)多個(gè)實(shí)例就會(huì)出現(xiàn)副作用隔盛,可以采用單例模式犹菱。具體如下:
- 要求生成唯一序列號(hào)的環(huán)境。
- 在整個(gè)項(xiàng)目中需要共享一個(gè)訪問(wèn)點(diǎn)或者數(shù)據(jù)吮炕。
- 創(chuàng)建和銷毀一個(gè)對(duì)象需要消耗的資源過(guò)多腊脱,但是又經(jīng)常用到。如,要訪問(wèn)IO和數(shù)據(jù)庫(kù)連接等们豌。
- 需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境,可以采用單例模式(也可以直接聲明為static)开呐。
- 需要頻繁的進(jìn)行創(chuàng)建和銷毀的對(duì)象杜耙。
4 單例模式的最佳實(shí)踐
單例模式比較簡(jiǎn)單搜骡,也應(yīng)用廣泛。在Spring中佑女,每個(gè)Bean默認(rèn)是單例的记靡,優(yōu)點(diǎn)是Spring容器可以管理這些Bean的生命周期,決定對(duì)象的創(chuàng)建和銷毀時(shí)機(jī)团驱,以及創(chuàng)建和銷毀對(duì)象時(shí)的處理摸吠。
5 單例模式常見(jiàn)的實(shí)現(xiàn)方式
單例模式的實(shí)現(xiàn)可以分為兩類:餓漢式(饑漢式)和懶漢式。
餓漢式:在程序啟動(dòng)或單例模式類被加載的時(shí)候店茶,單例模式實(shí)例就已經(jīng)被創(chuàng)建蜕便。
懶漢式:當(dāng)程序第一次訪問(wèn)單例模式實(shí)例的時(shí)候才進(jìn)行創(chuàng)建劫恒。
以上兩種方式各有優(yōu)點(diǎn)贩幻。
- 如果單例模式實(shí)例在系統(tǒng)中會(huì)被頻繁用到,餓漢式比較好
優(yōu)點(diǎn):程序啟動(dòng)的時(shí)候已經(jīng)進(jìn)行了實(shí)例化两嘴,調(diào)用時(shí)直接返回實(shí)例丛楚,速度快,效率高憔辫。
缺點(diǎn):如果實(shí)例使用頻率不高或者幾乎不用趣些,啟動(dòng)的時(shí)候就進(jìn)行實(shí)例化,浪費(fèi)內(nèi)存資源贰您。 - 如果單例模式實(shí)例在系統(tǒng)中很少用到或者幾乎不會(huì)用到坏平,懶漢式較好
優(yōu)點(diǎn):如果實(shí)例使用頻率不高或者幾乎不用,啟動(dòng)的時(shí)候就不進(jìn)行實(shí)例化锦亦,第一次調(diào)用的時(shí)候進(jìn)行實(shí)例化(lazy-loading)舶替,節(jié)約內(nèi)存資源。
缺點(diǎn):?jiǎn)卫J降膶?shí)例如果被頻繁調(diào)用杠园,影響效率顾瞪。
5.1 餓漢式常見(jiàn)實(shí)現(xiàn)
5.1.1 推薦的實(shí)現(xiàn)(線程安全)
- 靜態(tài)變量初始化(最推薦)
public class Singleton(){
private static Singleton instance = new Singleton();
// 私有構(gòu)造函數(shù)
private Singleton(){
}
public static Singleton getInstance(){
return instance ;
}
}
- 靜態(tài)代碼塊初始化(類似于1):
public class Singleton(){
private static Singleton instance;
{
instance = new Singleton();
}
// 私有構(gòu)造函數(shù)
private Singleton(){
}
public static Singleton getInstance(){
return instance ;
}
}
- 枚舉類
單實(shí)例枚舉類SingletonEnum :
public enum Singleton {
/**
* 實(shí)例
*/
INSTANCE;
private Singleton() {
}
/**
* 業(yè)務(wù)方法
*/
public void doSomething() {
//TODO 業(yè)務(wù)代碼
}
}
使用:
public class SingletonDemo {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();
}
}
默認(rèn)枚舉實(shí)例的創(chuàng)建是線程安全的,但是在枚舉中的其他任何方法由程序員自己負(fù)責(zé)抛蚁。如果你正在使用實(shí)例方法陈醒,那么你需要確保線程安全(如果它影響到其他對(duì)象的狀態(tài)的話)。
傳統(tǒng)單例存在的另外一個(gè)問(wèn)題是一旦你實(shí)現(xiàn)了序列化接口瞧甩,那么它們不再保持單例钉跷。但是枚舉單例,JVM對(duì)序列化有保證肚逸。
優(yōu)點(diǎn):有序列化和線程安全的保證尘应,代碼簡(jiǎn)單惶凝。
5.2 懶漢式常見(jiàn)實(shí)現(xiàn)
- 單次判斷實(shí)例為null
適合單線程,不適合多線程【線程不安全】犬钢。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
這種寫(xiě)法起到了Lazy Loading的效果苍鲜,但是只能在單線程下使用。如果在多線程下玷犹,一個(gè)線程進(jìn)入了 if (singleton == null) 判斷語(yǔ)句塊混滔,還未來(lái)得及往下執(zhí)行,另一個(gè)線程也通過(guò)了這個(gè)判斷語(yǔ)句歹颓,這時(shí)便會(huì)產(chǎn)生多個(gè)實(shí)例坯屿。所以在多線程環(huán)境下不可使用這種方式。
- 同步方法獲取實(shí)例
線程安全巍扛,但是效率低领跛,不推薦使用。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
解決上面第1種實(shí)現(xiàn)方式的線程不安全問(wèn)題撤奸,做個(gè)線程同步就可以了吠昭,于是就對(duì)getInstance()方法進(jìn)行了線程同步。
缺點(diǎn):效率太低胧瓜。每個(gè)線程在想獲得類的實(shí)例時(shí)候矢棚,執(zhí)行g(shù)etInstance()方法都要進(jìn)行同步。而其實(shí)這個(gè)方法只執(zhí)行一次實(shí)例化代碼就夠了府喳,后面的想獲得該類實(shí)例蒲肋,直接return就行了。方法進(jìn)行同步效率太低要改進(jìn)钝满。
- 單次判斷實(shí)例為null兜粘,同步代碼塊生成實(shí)例【不推薦使用】
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
由于第2種實(shí)現(xiàn)方式同步效率太低,所以摒棄同步方法弯蚜,改為同步產(chǎn)生實(shí)例化的的代碼塊孔轴。但是這種同步并不能起到線程同步的作用。跟第1種實(shí)現(xiàn)方式遇到的情形一致熟吏。假如一個(gè)線程進(jìn)入了 if (singleton == null) 判斷語(yǔ)句塊距糖,還未來(lái)得及往下執(zhí)行,另一個(gè)線程也通過(guò)了這個(gè)判斷語(yǔ)句牵寺,這時(shí)便會(huì)產(chǎn)生多個(gè)實(shí)例悍引。
- 雙重檢查【推薦使用】
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
Double-Check概念對(duì)于多線程開(kāi)發(fā)者來(lái)說(shuō)不會(huì)陌生。進(jìn)行兩次 if (singleton == null) 檢查帽氓,這樣就可以保證線程安全了趣斤。實(shí)例化代碼只用執(zhí)行一次,后面再次訪問(wèn)時(shí)黎休,判斷 if (singleton == null)浓领,直接return實(shí)例化對(duì)象玉凯。
優(yōu)點(diǎn):線程安全;延遲加載联贩;效率較高漫仆。
- 靜態(tài)內(nèi)部類【推薦使用】
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
}
這種方式跟餓漢式方式采用的機(jī)制類似,但又有不同泪幌。兩者都是采用了類裝載的機(jī)制來(lái)保證初始化實(shí)例時(shí)只有一個(gè)線程盲厌。不同的地方在餓漢式方式是只要Singleton類被裝載就會(huì)實(shí)例化,沒(méi)有Lazy-Loading的作用祸泪,而靜態(tài)內(nèi)部類方式在Singleton類被裝載時(shí)并不會(huì)立即實(shí)例化吗浩,而是在需要實(shí)例化時(shí),調(diào)用getInstance方法没隘,才會(huì)裝載SingletonInstance類懂扼,從而完成Singleton的實(shí)例化。
類的靜態(tài)屬性只會(huì)在第一次加載類的時(shí)候初始化右蒲,所以在這里阀湿,JVM幫助我們保證了線程的安全性,在類進(jìn)行初始化時(shí)品嚣,別的線程是無(wú)法進(jìn)入的炕倘。
優(yōu)點(diǎn):避免了線程不安全钧大,延遲加載翰撑,效率高。
6 相關(guān)知識(shí)補(bǔ)充
6.1 關(guān)于延遲初始化(lazy loaded)
原則:“除非絕對(duì)必要啊央,否則就不要延遲初始化”眶诈。
延遲初始化是一把雙刃劍,它降低了初始化類或者創(chuàng)建實(shí)例的開(kāi)銷瓜饥,卻增加了訪問(wèn)被延遲初始化的域的開(kāi)銷逝撬,考慮到延遲初始化的域最終需要初始化的開(kāi)銷以及域的訪問(wèn)開(kāi)銷,延遲初始化實(shí)際上降低了性能乓土。