單例模式
單例模式是設(shè)計模式中最簡單的設(shè)計模式之一。他和工廠模式同屬于創(chuàng)建型模式,都用于類的實例化。不過兩者的區(qū)別很大,要解決的問題也不一樣。
單例模式保證一個類只會被實例化一次,使用的時候通過單例提供的方法來獲取實例。在確保線程安全的前提下,很多時候我們只需要同一個類的一個實例即可,而不是在任何使用的地方都實例化一個新對象。新對象創(chuàng)建是有成本的,不但要花時間,而且占用內(nèi)存。另外有的時候我們需要一個全局唯一的實例,比如計數(shù)器,全局多個計數(shù)器就會計數(shù)混亂不準確,如下圖所示。單例模式就是為了實現(xiàn)全局一個實例的需求。
1. 實現(xiàn)單例模
實現(xiàn)單例模式,其實我們需要實現(xiàn)如下需求:
- 提供獲取實例的方法。此方法會控制全局僅有一個實例,而不會重復(fù)創(chuàng)建實例;
- 全局唯一的實例要有地方能存放起來;
- 不能隨意通過new關(guān)鍵字創(chuàng)建實例。這樣才能控制調(diào)用方只能用受控的方法來創(chuàng)建對象。
針對以上三點需求我們需要做如下事情:
- 編寫一個獲取實例的公有方法,已經(jīng)創(chuàng)建過實例就直接返回實例,否則進行實例化;
- 實例化好的對象存哪里呢?存在類當中是最好的。這樣不用引入新的類,而且也符合就近原則;
- 禁止通過new關(guān)鍵字初始化,只需要把無參構(gòu)造方法私有化。此外不要添加任何有參數(shù)的構(gòu)造方法。
我們按照上面的思路實現(xiàn)第一版單例模式,代碼如下:
public class SingletonOne {
private static SingletonOne singletonOne;
private SingletonOne() {
}
public static SingletonOne getInstance() {
if (singletonOne == null) {
singletonOne = new SingletonOne();
}
return singletonOne;
}
}
代碼中使用靜態(tài)變量,也稱之為類變量保存SingletonOne
的實例。無參構(gòu)造方法私有化,并且不提供其他構(gòu)造方法。getInstance() 對外提供獲取實例的方法。方法內(nèi)部也符合我們的需求,已經(jīng)實例化,直接返回實例,如果還是null,去創(chuàng)建這個實例。這種方式稱之為懶漢式,是因為類的實例化延遲到第一次getInstance的時候。
看起來上面的代碼實現(xiàn)了我們提到的三點需求,無懈可擊。沒錯,一般的場景采用上面的代碼足以應(yīng)付。但是在并發(fā)的時候,上面的代碼是有問題的。并發(fā)時,兩個線程對于 singletonOne == null 的判斷可能都滿足,那么接下來每個線程各自都創(chuàng)建了一個實例。這和單例模式的目標是相違背的。我們需要改造一下。
1.1 線程安全的懶漢單例模式
想要線程安全還不好說,加上 Synchronized 關(guān)鍵字就可以了。修改后代碼如下:
public class SingletonTwo {
private static SingletonTwo singletonTwo;
private SingletonTwo() {
}
public static SingletonTwo getInstance() {
if (singletonTwo == null) {
synchronized (SingletonTwo.class) {
if (singletonTwo == null) {
singletonTwo = new SingletonTwo();
}
}
}
return singletonTwo;
}
}
實例化之前為了確保線程安全,我們加上了 synchronized 關(guān)鍵字。你肯定注意到 synchronized 代碼塊中,又判斷了一次 singletonTwo 是否為 null。這是因為你在等待鎖的這段時間,可能其他線程已經(jīng)完成了實例化。所以此處加上 null 的判斷,才能確保全局唯一!
看到這里你一定贊嘆,這是多么嚴謹?shù)某绦?,一定不會有錯了!但是事實卻不是這樣。
如果你學(xué)習(xí)過多線程,一定對重排序有印象。CPU 為了提高運行效率,可能會對編譯后代碼的指令做優(yōu)化,這些優(yōu)化不能保證代碼執(zhí)行完全符合編寫的順序。但是一定能保證代碼執(zhí)行的結(jié)果和按照編寫順序執(zhí)行的結(jié)果是一致的。重排序在單線程下沒有任何問題,不過多線程就會出問題了。其實解決方法也很簡單,只需要為
singletonTwo 聲明時加上 volatile 關(guān)鍵字即可。volatile 修飾的變量是會保證讀操作一定能讀到寫完的值。這種單例也叫做雙重檢查模式。
代碼如下:
public class SingletonTwo {
private volatile static SingletonTwo singletonTwo;
private SingletonTwo() {
}
public static SingletonTwo getInstance() {
if (singletonTwo == null) {
synchronized (SingletonTwo.class) {
if (singletonTwo == null) {
singletonTwo = new SingletonTwo();
}
}
}
return singletonTwo;
}
}
1.2 餓漢式單例模式
有懶漢就有餓漢。餓漢式單例模式在類初實話的時候就會進行實例化。好處是不會有線程安全的問題。問題就是不管程序用不用,實例都早以創(chuàng)建好,這對內(nèi)存是種浪費。
代碼如下:
public class SingletonThree {
private static SingletonThree singletonOne = new SingletonThree();
private SingletonThree() {
}
public static SingletonThree getInstance() {
return singletonOne;
}
}
1.3 內(nèi)部靜態(tài)類方式
這次我們先看代碼:
public class SingletonFour {
private SingletonFour() {
}
public static SingletonFour getInstance() {
return SingletonHolder.singletonFour;
}
private static class SingletonHolder{
private static final SingletonFour singletonFour = new SingletonFour();
}
}
代碼中增加了內(nèi)部靜態(tài)類 SingletonHolder
,內(nèi)部有一個SingletonFour
的實例,并且也是類級別的。那這種方式是餓漢式還是懶漢式?看起來像是餓漢式,因為實例化也是在類初實話的時候進行的。但如果是餓漢式,為什么還要兜這個圈?
其實這是懶漢式。因為內(nèi)部靜態(tài)類是現(xiàn)在第一次使用的時候才會去初始化。所以SingletonHolder
最初并未被初始化。當?shù)谝淮螆?zhí)行 return SingletonHolder.singletonFour 時,才會去初始化SingletonHolder
類,從而實例化SingletonFour
。這種方式利用類加載的機制達到了雙重檢查模式的效果,而代碼更為簡潔。
2. 單例模式適用場景
- 必須保證全局一個實例。如計數(shù)器,多個實例計數(shù)就不準確了。再比如線程池,多個實例的話,管理就亂套了。
- 一個實例就能滿足程序不同地方的使用,并且是線程安全的。比如我們使用 Spring 開發(fā)的 bean,絕大多數(shù)都可以用單例模式。例如某個 service 類,因為自己不維護狀態(tài),線程安全,其實全局只需要一個實例。
- 對象被頻繁創(chuàng)建和銷毀,可以考慮使用單例。
- 對象創(chuàng)建比較消耗資源。
3. 小結(jié)
我們本節(jié)學(xué)習(xí)了四種單例的實現(xiàn)方式:
- 餓漢式非線程安全;
- 懶漢式線程安全(雙重檢查模式);
- 餓漢式單例模式;
- 內(nèi)部靜態(tài)類方式。
單例模式雖然簡單,但是想寫的嚴謹,還是需要考慮周全。實際使用中,推薦使用雙重檢查模式和內(nèi)部靜態(tài)類方式。如果實例在你的程序初始化階段就會被使用,也可以使用餓漢式。非線程安全的懶漢式只能用于非并發(fā)的場景,局限性比較大,并不推薦使用。