前言
今天介紹下單例模式,單例模式(Singleton Pattern)是 Java 中最簡(jiǎn)單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。
正文
舉個(gè)常見的單例模式例子,我們?nèi)粘J褂玫碾娔X上都有一個(gè)回收站,在整個(gè)操作系統(tǒng)中,回收站只能有一個(gè)實(shí)例,整個(gè)系統(tǒng)都使用這個(gè)唯一的實(shí)例,而且回收站自行提供自己的實(shí)例。因此回收站是單例模式的應(yīng)用。
單例模式概念
單例模式(Singleton Pattern):確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例,這個(gè)類稱為單例類,它提供全局訪問(wèn)的方法。單例模式是一種對(duì)象創(chuàng)建型模式。
單例模式結(jié)構(gòu)圖
單例模式是結(jié)構(gòu)最簡(jiǎn)單的設(shè)計(jì)模式一,在它的核心結(jié)構(gòu)中只包含一個(gè)被稱為單例類的特殊類。
單例模式有三個(gè)特性:
- 單例類只能有一個(gè)實(shí)例
- 單例類必須自行創(chuàng)建自己的唯一的實(shí)例
- 單例類必須給所有其他對(duì)象提供這一實(shí)例
單例模式結(jié)構(gòu)如圖所示:

單例模式結(jié)構(gòu)圖中只包含一個(gè)單例角色:
-
Singleton(單例):在單例類的內(nèi)部實(shí)現(xiàn)只生成一個(gè)實(shí)例,同時(shí)它提供一個(gè)靜態(tài)的getInstance()工廠方法,讓客戶可以訪問(wèn)它的唯一實(shí)例;為了防止在外部對(duì)其實(shí)例化,將其構(gòu)造函數(shù)設(shè)計(jì)為私有;在單例類內(nèi)部定義了一個(gè)Singleton類型的靜態(tài)對(duì)象,作為外部共享的唯一實(shí)例。
單例模式的幾種實(shí)現(xiàn)方式
單例模式的實(shí)現(xiàn)有多種方式,如下所示:
1、懶漢式,線程不安全
是否 Lazy 初始化:是
是否多線程安全:否
實(shí)現(xiàn)難度:易
描述:這種方式是最基本的實(shí)現(xiàn)方式,這種實(shí)現(xiàn)最大的問(wèn)題就是不支持多線程。因?yàn)闆](méi)有加鎖synchronized,所以嚴(yán)格意義上它并不算單例模式。
這種方式lazy loading很明顯,不要求線程安全,在多線程不能正常工作。
代碼實(shí)例:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
接下來(lái)介紹的幾種實(shí)現(xiàn)方式都支持多線程,但是在性能上有所差異。
2、懶漢式,線程安全
是否 Lazy 初始化:是
是否多線程安全:是
實(shí)現(xiàn)難度:易
描述:這種方式具備很好的lazy loading,能夠在多線程中很好的工作,但是,效率很低,99% 情況下不需要同步。
優(yōu)點(diǎn):第一次調(diào)用才初始化,避免內(nèi)存浪費(fèi)。
缺點(diǎn):必須加鎖synchronized才能保證單例,但加鎖會(huì)影響效率。
getInstance()的性能對(duì)應(yīng)用程序不是很關(guān)鍵(該方法使用不太頻繁)。
代碼實(shí)例:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3、餓漢式
是否 Lazy 初始化:否
是否多線程安全:是
實(shí)現(xiàn)難度:易
描述:這種方式比較常用,但容易產(chǎn)生垃圾對(duì)象。
優(yōu)點(diǎn):沒(méi)有加鎖,執(zhí)行效率會(huì)提高。
缺點(diǎn):類加載時(shí)就初始化,浪費(fèi)內(nèi)存。
它基于classloder機(jī)制避免了多線程的同步問(wèn)題,不過(guò),instance在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化instance顯然沒(méi)有達(dá)到lazy loading的效果。
代碼實(shí)例:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
4、雙檢鎖/雙檢查鎖(DCL,即 double-checked locking)
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多線程安全:是
實(shí)現(xiàn)難度:較復(fù)雜
描述:這種方式稱為雙重檢查鎖(Double-Check Locking),需要注意的是,如果使用雙重檢查鎖定來(lái)實(shí)現(xiàn)懶漢式單例類,需要在靜態(tài)成員變量instance之前增加修飾符volatile,被volatile修飾的成員變量可以確保多個(gè)線程都能夠正確處理,且該代碼只能在JDK 1.5及以上版本中才能正確執(zhí)行。由于volatile關(guān)鍵字會(huì)屏蔽Java虛擬機(jī)所做的一些代碼優(yōu)化,可能會(huì)導(dǎo)致系統(tǒng)運(yùn)行效率降低,因此即使使用雙重檢查鎖定來(lái)實(shí)現(xiàn)單例模式也不是一種完美的實(shí)現(xiàn)方式。
代碼實(shí)例:
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
5、靜態(tài)內(nèi)部類
是否 Lazy 初始化:是
是否多線程安全:是
實(shí)現(xiàn)難度:一般
描述:餓漢式單例類不能實(shí)現(xiàn)延遲加載,不管將來(lái)用不用始終占據(jù)內(nèi)存;懶漢式單例類線程安全控制煩瑣,而且性能受影響??梢?,無(wú)論是餓漢式單例還是懶漢式單例都存在這樣那樣的問(wèn)題,有沒(méi)有一種方法,能夠?qū)煞N單例的缺點(diǎn)都克服,而將兩者的優(yōu)點(diǎn)合二為一呢?答案是:Yes!下面我們來(lái)學(xué)習(xí)這種更好的被稱之為Initialization Demand Holder (IoDH)的技術(shù)。在IoDH中,我們?cè)趩卫愔性黾右粋€(gè)靜態(tài)(static)內(nèi)部類,在該內(nèi)部類中創(chuàng)建單例對(duì)象,再將該單例對(duì)象通過(guò)getInstance()方法返回給外部使用。由于靜態(tài)單例對(duì)象沒(méi)有作為Singleton的成員變量直接實(shí)例化,因此類加載時(shí)不會(huì)實(shí)例化Singleton,第一次調(diào)用getInstance()時(shí)將加載內(nèi)部類SingletonHolder,在該內(nèi)部類中定義了一個(gè)static類型的變量instance,此時(shí)會(huì)首先初始化這個(gè)成員變量,由Java虛擬機(jī)來(lái)保證其線程安全性,確保該成員變量只能初始化一次。由于getInstance()方法沒(méi)有任何線程鎖定,因此其性能不會(huì)造成任何影響。通過(guò)使用IoDH,我們既可以實(shí)現(xiàn)延遲加載,又可以保證線程安全,不影響系統(tǒng)性能,不失為一種最好的Java語(yǔ)言單例模式實(shí)現(xiàn)方式**(其缺點(diǎn)是與編程語(yǔ)言本身的特性相關(guān),很多面向?qū)ο笳Z(yǔ)言不支持IoDH)。
代碼實(shí)例:
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
6、枚舉
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多線程安全:是
實(shí)現(xiàn)難度:易
描述:這種實(shí)現(xiàn)方式還沒(méi)有被廣泛采用,但這是實(shí)現(xiàn)單例模式的最佳方法。它更簡(jiǎn)潔,自動(dòng)支持序列化機(jī)制,絕對(duì)防止多次實(shí)例化。
這種方式是Effective Java作者Josh Bloch提倡的方式,它不僅能避免多線程同步問(wèn)題,而且還自動(dòng)支持序列化機(jī)制,防止反序列化重新創(chuàng)建新的對(duì)象,絕對(duì)防止多次實(shí)例化。不過(guò),由于 JDK1.5 之后才加入 enum 特性,用這種方式寫不免讓人感覺(jué)生疏,在實(shí)際工作中,也很少用。
不能通過(guò)reflection attack來(lái)調(diào)用私有構(gòu)造方法。
代碼實(shí)例:
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
經(jīng)驗(yàn)之談:一般情況下,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 3 種餓漢方式。只有在要明確實(shí)現(xiàn)lazy loading效果時(shí),才會(huì)使用第 5 種登記方式。如果涉及到反序列化創(chuàng)建對(duì)象時(shí),可以嘗試使用第 6 種枚舉方式。如果有其他特殊的需求,可以考慮使用第 4 種雙檢鎖方式。
Java中的語(yǔ)言中的單例模式
Java語(yǔ)言中就有很多單例模式的應(yīng)用實(shí)例,這里舉例一個(gè)。
Java的Runtime對(duì)象
在Java語(yǔ)言內(nèi)部,java.lang.Runtime對(duì)象就是一個(gè)使用單例模式的例子。在每一個(gè)Java應(yīng)用程序里面,都有唯一的一個(gè)Runtime對(duì)象,應(yīng)用程序可以與其運(yùn)行環(huán)境發(fā)生相互作用。
Runtime類提供一個(gè)靜態(tài)工廠方法getRuntime():
public static Runtime getRuntime();
通過(guò)調(diào)用此方法,可以獲得Runtime類唯一的一個(gè)實(shí)例:
Runtime rt=Runtime.getRuntime();
總結(jié)
單例模式作為一種目標(biāo)明確、結(jié)構(gòu)簡(jiǎn)單、理解容易的設(shè)計(jì)模式,在軟件開發(fā)中使用頻率相當(dāng)高,在很多應(yīng)用軟件和框架中都得以廣泛應(yīng)用。
1.主要優(yōu)點(diǎn)
單例模式的主要優(yōu)點(diǎn)如下:
- 單例模式提供了對(duì)唯一實(shí)例的受控訪問(wèn)。因?yàn)閱卫惙庋b了它的唯一實(shí)例,所以它可以嚴(yán)格控制客戶怎樣以及何時(shí)訪問(wèn)它。
- 由于在系統(tǒng)內(nèi)存中只存在一個(gè)對(duì)象,因此可以節(jié)約系統(tǒng)資源,對(duì)于一些需要頻繁創(chuàng)建和銷毀的對(duì)象單例模式無(wú)疑可以提高系統(tǒng)的性能。
- 允許可變數(shù)目的實(shí)例?;趩卫J轿覀兛梢赃M(jìn)行擴(kuò)展,使用與單例控制相似的方法來(lái)獲得指定個(gè)數(shù)的對(duì)象實(shí)例,既節(jié)省系統(tǒng)資源,又解決了單例單例對(duì)象共享過(guò)多有損性能的問(wèn)題。
2.主要缺點(diǎn)
單例模式的主要缺點(diǎn)如下:
- 由于單例模式中沒(méi)有抽象層,因此單例類的擴(kuò)展有很大的困難。
- 單例類的職責(zé)過(guò)重,在一定程度上違背了“單一職責(zé)原則”。因?yàn)閱卫惣瘸洚?dāng)了工廠角色,提供了工廠方法,同時(shí)又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起。
- 現(xiàn)在很多面向?qū)ο笳Z(yǔ)言(如Java、C#)的運(yùn)行環(huán)境都提供了自動(dòng)垃圾回收的技術(shù),因此,如果實(shí)例化的共享對(duì)象長(zhǎng)時(shí)間不被利用,系統(tǒng)會(huì)認(rèn)為它是垃圾,會(huì)自動(dòng)銷毀并回收資源,下次利用時(shí)又將重新實(shí)例化,這將導(dǎo)致共享的單例對(duì)象狀態(tài)的丟失。
3.適用場(chǎng)景
在以下情況下可以考慮使用單例模式:
- 系統(tǒng)只需要一個(gè)實(shí)例對(duì)象,如系統(tǒng)要求提供一個(gè)唯一的序列號(hào)生成器或資源管理器,或者需要考慮資源消耗太大而只允許創(chuàng)建一個(gè)對(duì)象。
- 客戶調(diào)用類的單個(gè)實(shí)例只允許使用一個(gè)公共訪問(wèn)點(diǎn),除了該公共訪問(wèn)點(diǎn),不能通過(guò)其他途徑訪問(wèn)該實(shí)例。
一直覺(jué)得自己寫的不是技術(shù),而是情懷,一篇篇文章是自己這一路走來(lái)的痕跡。靠專業(yè)技能的成功是最具可復(fù)制性的,希望我的這條路能讓你少走彎路,希望我能幫你抹去知識(shí)的蒙塵,希望我能幫你理清知識(shí)的脈絡(luò),希望未來(lái)技術(shù)之巔上有你也有我。