单例模式(Singleton-Pattern)百媚生
1 動(dòng)機(jī)
對于系統(tǒng)中的某些類來說,只有一個(gè)實(shí)例很重要,例如,一個(gè)系統(tǒng)中可以存在多個(gè)打印任務(wù),但是只能有一個(gè)正在工作的任務(wù);一個(gè)系統(tǒng)只能有一個(gè)窗口管理器或文件系統(tǒng);一個(gè)系統(tǒng)只能有一個(gè)計(jì)時(shí)工具或ID(序號(hào))生成器。
如何保證一個(gè)類只有一個(gè)實(shí)例并且這個(gè)實(shí)例易于被訪問呢?定義一個(gè)全局變量可以確保對象隨時(shí)都可以被訪問,但不能防止我們實(shí)例化多個(gè)對象。
一個(gè)更好的解決辦法是讓類自身負(fù)責(zé)保存它的唯一實(shí)例。這個(gè)類可以保證沒有其他實(shí)例被創(chuàng)建,并且它可以提供一個(gè)訪問該實(shí)例的方法。這就是單例模式的模式動(dòng)機(jī)。
#2 定義
單例模式確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例,這個(gè)類稱為單例類,它提供全局訪問的方法。
單例模式的要點(diǎn)有三個(gè)
某個(gè)類只能有一個(gè)實(shí)例
它必須自行創(chuàng)建這個(gè)實(shí)例
它必須自行向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。單例模式是一種對象創(chuàng)建型模式。單例模式又名單件模式或單態(tài)模式。
優(yōu)點(diǎn): 1、在內(nèi)存里只有一個(gè)實(shí)例,減少了內(nèi)存的開銷,尤其是頻繁的創(chuàng)建和銷毀實(shí)例(比如管理學(xué)院首頁頁面緩存)。 2、避免對資源的多重占用(比如寫文件操作)。
**缺點(diǎn):**沒有接口,不能繼承,與單一職責(zé)原則沖突,一個(gè)類應(yīng)該只關(guān)心內(nèi)部邏輯,而不關(guān)心外面怎么樣來實(shí)例化
使用場景:
1、要求生產(chǎn)唯一序列號(hào)。
2、WEB 中的計(jì)數(shù)器,不用每次刷新都在數(shù)據(jù)庫里加一次,用單例先緩存起來。
3、創(chuàng)建的一個(gè)對象需要消耗的資源過多,比如 I/O 與數(shù)據(jù)庫的連接等。
注意事項(xiàng): getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時(shí)進(jìn)入造成 instance 被多次實(shí)例化。
結(jié)構(gòu)
分析
單例模式的目的是保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)訪問它的全局訪問點(diǎn)。單例模式包含的角色只有一個(gè),就是單例類——Singleton。單例類擁有一個(gè)私有構(gòu)造函數(shù),確保用戶無法通過new關(guān)鍵字直接實(shí)例化它。除此之外,該模式中包含一個(gè)靜態(tài)私有成員變量與靜態(tài)公有的工廠方法,該工廠方法負(fù)責(zé)檢驗(yàn)實(shí)例的存在性并實(shí)例化自己,然后存儲(chǔ)在靜態(tài)成員變量中,以確保只有一個(gè)實(shí)例被創(chuàng)建。
在單例模式的實(shí)現(xiàn)過程中,需要注意如下三點(diǎn):
單例類的構(gòu)造函數(shù)為私有;
提供一個(gè)自身的靜態(tài)私有成員變量;
提供一個(gè)公有的靜態(tài)工廠方法。
優(yōu)點(diǎn)
提供了對唯一實(shí)例的受控訪問。因?yàn)閱卫惙庋b了它的唯一實(shí)例,所以它可以嚴(yán)格控制客戶怎樣以及何時(shí)訪問它,并為設(shè)計(jì)及開發(fā)團(tuán)隊(duì)提供了共享的概念
由于在系統(tǒng)內(nèi)存中只存在一個(gè)對象,因此可以節(jié)約系統(tǒng)資源,對于一些需要頻繁創(chuàng)建和銷毀的對象,單例模式無疑可以提高系統(tǒng)的性能。
允許可變數(shù)目的實(shí)例。我們可以基于單例模式進(jìn)行擴(kuò)展,使用與單例控制相似的方法來獲得指定個(gè)數(shù)的對象實(shí)例。
缺點(diǎn)
由于單例模式中沒有抽象層,因此單例類的擴(kuò)展困難
單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。
因?yàn)閱卫惣瘸洚?dāng)了工廠角色,提供了工廠方法,同時(shí)又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起。
濫用單例將帶來一些負(fù)面問題,如
為了節(jié)省資源將數(shù)據(jù)庫連接池對象設(shè)計(jì)為單例類,可能會(huì)導(dǎo)致共享連接池對象的程序過多而出現(xiàn)連接池溢出
現(xiàn)在很多面向?qū)ο笳Z言的運(yùn)行環(huán)境都提供了自動(dòng)垃圾回收的技術(shù),因此,如果實(shí)例化的對象長時(shí)間不被利用,系統(tǒng)會(huì)認(rèn)為它是垃圾,會(huì)自動(dòng)銷毀并回收資源,下次利用時(shí)又將重新實(shí)例化,這將導(dǎo)致對象狀態(tài)的丟失。
#適用場景
系統(tǒng)只需要一個(gè)實(shí)例對象,如
系統(tǒng)要求提供一個(gè)唯一的序列號(hào)生成器
需要考慮資源消耗太大而只允許創(chuàng)建一個(gè)對象
客戶調(diào)用類的單個(gè)實(shí)例只允許使用一個(gè)公共訪問點(diǎn),除了該公共訪問點(diǎn),不能通過其他途徑訪問該實(shí)例。
在一個(gè)系統(tǒng)中要求一個(gè)類只有一個(gè)實(shí)例時(shí)才應(yīng)當(dāng)使用單例模式。反過來,如果一個(gè)類可以有幾個(gè)實(shí)例共存,就需要對單例模式進(jìn)行改進(jìn),使之成為多例模式
應(yīng)用
一個(gè)具有自動(dòng)編號(hào)主鍵的表可以有多個(gè)用戶同時(shí)使用,但數(shù)據(jù)庫中只能有一個(gè)地方分配下一個(gè)主鍵編號(hào),否則會(huì)出現(xiàn)主鍵重復(fù),因此該主鍵編號(hào)生成器必須具備唯一性,可以通過單例模式來實(shí)現(xiàn)。
總結(jié)
單例模式確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例,這個(gè)類稱為單例類,它提供全局訪問的方法。單例模式的要點(diǎn)有三個(gè):一是某個(gè)類只能有一個(gè)實(shí)例;二是它必須自行創(chuàng)建這個(gè)實(shí)例;三是它必須自行向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。單例模式是一種對象創(chuàng)建型模式。
單例模式只包含一個(gè)單例角色:在單例類的內(nèi)部實(shí)現(xiàn)只生成一個(gè)實(shí)例,同時(shí)它提供一個(gè)靜態(tài)的工廠方法,讓客戶可以使用它的唯一實(shí)例;為了防止在外部對其實(shí)例化,將其構(gòu)造函數(shù)設(shè)計(jì)為私有。
單例模式的目的是保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)訪問它的全局訪問點(diǎn)。單例類擁有一個(gè)私有構(gòu)造函數(shù),確保用戶無法通過new關(guān)鍵字直接實(shí)例化它。除此之外,該模式中包含一個(gè)靜態(tài)私有成員變量與靜態(tài)公有的工廠方法。該工廠方法負(fù)責(zé)檢驗(yàn)實(shí)例的存在性并實(shí)例化自己,然后存儲(chǔ)在靜態(tài)成員變量中,以確保只有一個(gè)實(shí)例被創(chuàng)建。
單例模式的主要優(yōu)點(diǎn)在于提供了對唯一實(shí)例的受控訪問并可以節(jié)約系統(tǒng)資源;其主要缺點(diǎn)在于因?yàn)槿鄙俪橄髮佣y以擴(kuò)展,且單例類職責(zé)過重。
單例模式適用情況包括:系統(tǒng)只需要一個(gè)實(shí)例對象;客戶調(diào)用類的單個(gè)實(shí)例只允許使用一個(gè)公共訪問點(diǎn)。
實(shí)現(xiàn)方式
1、懶漢式(非線程安全)
最基本的實(shí)現(xiàn)方式
不支持多線程。因?yàn)闆]有加鎖 synchronized,所以嚴(yán)格意義上并不算單例模式
這種方式 lazy loading 很明顯,不要求線程安全,當(dāng)有多個(gè)線程并行調(diào)用 getInstance() 的時(shí)候,就會(huì)創(chuàng)建多個(gè)實(shí)例。也就是說在多線程下不能正常工作。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2、懶漢式(線程安全)
為了解決上面的問題,最簡單的方法是將整個(gè) getInstance() 方法設(shè)為同步(synchronized)
優(yōu)點(diǎn):第一次調(diào)用才初始化,避免內(nèi)存浪費(fèi)。
缺點(diǎn):必須加鎖 synchronized 才能保證單例,但加鎖會(huì)影響效率。
雖然做到了線程安全,并解決了多實(shí)例的問題,但并不高效。
因?yàn)樵谌魏螘r(shí)候只能有一個(gè)線程調(diào)用 getInstance()
但是同步操作只需要在第一次調(diào)用時(shí)才被需要,即第一次創(chuàng)建單例實(shí)例對象時(shí)。
這就引出了雙重檢驗(yàn)鎖。
public class Singleton {
private static volatile Singleton INSTANCE = null;
// Private constructor suppresses
// default public constructor
private Singleton() {}
//thread safe and performance promote
public static Singleton getInstance() {
if(INSTANCE == null){
synchronized(Singleton.class){
//when more than two threads run into the first null check same time, to avoid instanced more than one time, it needs to be checked again.
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
##3 餓漢式
較常用,但易產(chǎn)生垃圾對象
優(yōu)點(diǎn):無鎖,執(zhí)行效率提高
缺點(diǎn):類加載時(shí)就初始化,浪費(fèi)內(nèi)存
非常簡單,實(shí)例被聲明成 static 和 final變量了,在第一次加載類到內(nèi)存中時(shí)就會(huì)初始化,所以創(chuàng)建實(shí)例本身是線程安全的。
它基于類加載機(jī)制避免了多線程的同步問題
不過,instance在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用 getInstance, 但也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化instance 顯然沒有達(dá)到lazy loading
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
// Private constructor suppresses
private Singleton() {}
// default public constructor
public static Singleton getInstance() {
return INSTANCE;
}
}
這種寫法如果完美的話,就沒必要在啰嗦那么多雙檢鎖的問題了。
缺點(diǎn)是它不是一種懶加載模式(lazy initialization),單例會(huì)在加載類后一開始就被初始化,即使客戶端沒有調(diào)用 getInstance()方法。
餓漢式的創(chuàng)建方式在一些場景中將無法使用:譬如 Singleton 實(shí)例的創(chuàng)建是依賴參數(shù)或者配置文件的,在 getInstance() 之前必須調(diào)用某個(gè)方法設(shè)置參數(shù)給它,那樣這種單例寫法就無法使用了。
4、雙重檢驗(yàn)鎖模式(double checked locking pattern)
一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,因?yàn)闀?huì)有兩次檢查instance == null
一次是在同步塊外
一次是在同步塊內(nèi)。為什么在同步塊內(nèi)還要再檢驗(yàn)一次?因?yàn)榭赡軙?huì)有多個(gè)線程一起進(jìn)入同步塊外的 if,如果在同步塊內(nèi)不進(jìn)行二次檢驗(yàn)的話就會(huì)生成多個(gè)實(shí)例了。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) { //Single Checked
synchronized (Singleton.class) {
if (singleton == null) { //Double Checked
singleton = new Singleton();
}
}
}
return singleton;
}
}
看起來很完美,很可惜哦,它還是有問題。
主要在于
instance = new Singleton()
并非一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情
1、memory = allocate() 分配對象的內(nèi)存空間
2、ctorInstance() 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
3、instance = memory 設(shè)置instance指向剛分配的內(nèi)存執(zhí)行完這步 instance 就為非 null 了)
JVM和CPU優(yōu)化,發(fā)生了指令重排
但是在 JVM 的JIT 中存在指令重排序的優(yōu)化。
也就是說上面的第2步和第3步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者
1、memory = allocate() 分配對象的內(nèi)存空間
3、instance = memory 設(shè)置instance指向剛分配的內(nèi)存
2、ctorInstance() 初始化對象
則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí) instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 instance,然后使用,然后順理成章地報(bào)錯(cuò)。
只需要將 instance 變量聲明成volatile
public class Singleton {
private volatile static Singleton instance; //聲明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
有些人認(rèn)為使用 volatile 的原因是可見性,也就是可以保證線程在本地不會(huì)存有 instance 的副本,每次都是去主內(nèi)存中讀取。
但其實(shí)是不對的。使用 volatile 的主要原因是其另一個(gè)特性:禁止指令重排序優(yōu)化。
在 volatile 變量的賦值操作后面會(huì)有一個(gè)內(nèi)存屏障(生成的匯編代碼上),讀操作不會(huì)被重排序到內(nèi)存屏障之前。
比如上面的例子,取操作必須在執(zhí)行完 1-2-3 之后或者 1-3-2 之后,不存在執(zhí)行到 1-3 然后取到值的情況。從「先行發(fā)生原則」的角度理解的話,就是對于一個(gè) volatile 變量的寫操作都先行發(fā)生于后面對這個(gè)變量的讀操作(這里的“后面”是時(shí)間上的先后順序)。
但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 內(nèi)存模型)是存在缺陷的,即時(shí)將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前后的代碼仍然存在重排序問題。這個(gè) volatile 屏蔽重排序的問題在 Java 5 中才得以修復(fù),所以在這之后才可以放心使用 volatile。
相信你不會(huì)喜歡這種復(fù)雜又隱含問題的方式,當(dāng)然我們有更好的實(shí)現(xiàn)線程安全的單例模式的辦法。
#5、靜態(tài)內(nèi)部類
線程安全
實(shí)現(xiàn)難度: 一般
描述: 這種方式能達(dá)到雙檢鎖方式一樣的功效,但實(shí)現(xiàn)更簡單
對靜態(tài)域使用延遲初始化,應(yīng)使用這種方式而不是雙檢鎖方式
這種方式只適用于靜態(tài)域的情況,雙檢鎖方式可在實(shí)例域需要延遲初始化時(shí)使用。
這種方式同樣利用了 classloder 機(jī)制來保證初始化 instance 時(shí)只有一個(gè)線程,它跟第 3 種方式不同的是:第 3 種方式只要 Singleton 類被裝載了,那么 instance 就會(huì)被實(shí)例化(沒有達(dá)到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不一定被初始化。因?yàn)?SingletonHolder 類沒有被主動(dòng)使用,只有通過顯式調(diào)用 getInstance 方法時(shí),才會(huì)顯式裝載 SingletonHolder 類,從而實(shí)例化 instance。想象一下,如果實(shí)例化 instance 很消耗資源,所以想讓它延遲加載,另外一方面,又不希望在 Singleton 類加載時(shí)就實(shí)例化,因?yàn)椴荒艽_保 Singleton 類還可能在其他的地方被主動(dòng)使用從而被加載,那么這個(gè)時(shí)候?qū)嵗?instance 顯然是不合適的。這個(gè)時(shí)候,這種方式相比第 3 種方式就顯得很合理
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 枚舉
JDK5 起
線程安全
實(shí)現(xiàn)單例模式的最佳方法。
它更簡潔,自動(dòng)支持序列化機(jī)制,絕對防止多次實(shí)例化。
Effective Java 作者 Josh Bloch 提倡的方式,它不僅能
避免多線程同步問題
自動(dòng)支持序列化機(jī)制
防止反序列化重新創(chuàng)建新的對象
絕對防止多次實(shí)例化
不能通過 reflection attack 來調(diào)用私有構(gòu)造方法。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
經(jīng)驗(yàn)
一般情況下,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 3 種餓漢方式。
只有在要明確實(shí)現(xiàn) lazy loading 效果時(shí),才會(huì)使用第 5 種登記方式。
如果涉及到反序列化創(chuàng)建對象時(shí),可以嘗試使用第 6 種枚舉方式。
如果有其他特殊的需求,可以考慮使用第 4 種雙檢鎖方式。
?
轉(zhuǎn)載于:https://juejin.im/post/5c3465a36fb9a049a81f7fac
總結(jié)
以上是生活随笔為你收集整理的单例模式(Singleton-Pattern)百媚生的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C/C++服务器开发的必备利器–libc
- 下一篇: Ceph分布式存储高性能设计