在項目開發(fā)時有一些對象其實我們只需要一個,比如:線程池、緩存、日志對象等等。這類對象只能有一個實例,如果制造出多個實例,就會導致許多問題產(chǎn)生,例如:程序的行為異常,資源使用過量,或者是不一致的結(jié)果。
雖然程序員之間的約定以及全局變量也可以辦得到,但是單例模式確實是經(jīng)得起時間考驗的更好的做法。單例模式和全局變量一樣方便的給我們提供了一個全局的訪問點,但是也解決了全局變量必須在程序一開始就要創(chuàng)建好對象的缺點。單例模式可以靈活的決定對象什么時候創(chuàng)建。
結(jié)構定義

單例模式: 保證一個類僅有一個實例,并且提供一個訪問它的全局訪問點。
通常我們可以讓一個全局變量使得一個對象被訪問,但它不能防止你實例化多個對象。一個最好的辦法就是讓類自身負責保存它的唯一實例。這個類可以保證沒有其它實例可以被創(chuàng)建,并且它可以提供一個訪問該實例的方法。[DP]
單例模式的寫法(7種)
單例模式的思路
- 利用一個靜態(tài)變量
INSTANCE來記錄類的唯一實例 - 把構造器聲明為私有的,只有在類本身才能調(diào)用構造器
- 用
getInstance()方法實例化對象,并返回這個類的實例
分析:
利用靜態(tài)變量來保存類的實例確保該實例為類的唯一實例,如果實例為空,則表示還沒有創(chuàng)建實例,而如果不存在我們就利用私有的構造器產(chǎn)生一個該類實例并把它賦值到靜態(tài)變量中,如果我們不需要這個實例,它就永遠不會產(chǎn)生。這個就是延遲實例化
- 懶漢模式(線程不安全)
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
這段代碼簡單明了,而且使用了懶加載模式,但是卻存在致命的問題。當有多個線程并行調(diào)用 getInstance() 的時候,就會創(chuàng)建多個實例。也就是說在多線程下不能正常工作。
-
懶漢模式(線程安全)
解決懶漢模式線程安全問題,最簡單的方法是將整個getInstance()方法設為同步synchronized。
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
雖然做到了線程安全,并且解決了多實例的問題,但是它并不高效。因為在任何時候只能有一個線程調(diào)用 getInstance()方法。但是同步操作只需要在第一次調(diào)用時才被需要,即第一次創(chuàng)建單例實例對象時。這就引出了雙重檢驗鎖。
-
雙重校驗鎖 *
雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,因為會有兩次檢查instance == null,一次是在同步塊外,一次是在同步塊內(nèi)。為什么在同步塊內(nèi)還要再檢驗一次?因為可能會有多個線程一起進入同步塊外的 if,如果在同步塊內(nèi)不進行二次檢驗的話就會生成多個實例了。
public static Singleton getInstance() {
if (INSTANCE == null) { // 一重
synchronized (Singleton.class) {
if (INSTANCE == null) { // 二重
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
這段代碼會有一個隱藏問題,主要是在INSTANCE = new Singleton()這句涉及到了JVM編譯器的指令重排,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:
- 給 instance 分配內(nèi)存
- 調(diào)用 Singleton 的構造函數(shù)來初始化成員變量
- 將instance對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。
我們只需要將 instance 變量聲明成 volatile 就可以了。
public class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {
}
/**
* 雙重校驗鎖
*/
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
關于volatile修飾符用最簡單的方式理解就是阻止了變量訪問前后的指令重排,保證了指令執(zhí)行順序。
- 餓漢模式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
這種方法非常簡單,因為單例的實例被聲明成 static 和 final 變量了,在第一次加載類到內(nèi)存中時就會初始化,所以創(chuàng)建實例本身是線程安全的。
這種方式基于classloder機制避免了多線程的同步問題,不過,instance在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導致類裝載,這時候初始化instance顯然沒有達到lazy loading的效果。
- 餓漢模式(變種)
private static Singleton instance;
static {
instance = new Singleton();
}
public static Singleton getInstance() {
return instance;
}
這種寫法本質(zhì)上和上一種寫法沒什么區(qū)別。
- 靜態(tài)內(nèi)部類
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
注意:
- 從外部無法訪問靜態(tài)內(nèi)部類SingletonHolder,只有當調(diào)用Singleton.getInstance方法的時候,才能得到單例對象INSTANCE。
- INSTANCE對象初始化的時機并不是在單例類Singleton被加載的時候,而是在調(diào)用getInstance方法,使得靜態(tài)內(nèi)部類SingletonHolder被加載的時候。因此這種實現(xiàn)方式是利用classloader的加載機制來實現(xiàn)懶加載,并保證構建單例的線程安全。
- 無法防止利用反射重復構建對象
-
枚舉高效寫法
在《Effective Java》最后推薦了這樣一個寫法,簡直有點顛覆,不僅超級簡單,而且保證了線程安全。這里引用一下,此方法無償提供了序列化機制,絕對防止多次實例化,及時面對復雜的序列化或者反射攻擊。單元素枚舉類型已經(jīng)成為實現(xiàn)Singleton的最佳方法。
public enum Singleton {
/**
*
*/
INSTANCE;
/**
*
*/
public void hello() {
System.out.println("Hello World");
}
}
對于一個標準的enum單例模式,最優(yōu)秀的寫法還是實現(xiàn)接口的形式:
public enum Singleton implements MySingleton {
/**
*
*/
INSTANCE {
@Override
public void hello() {
System.out.println("Hello world");
}
}
}
interface MySingleton {
/**
* xx
*/
void hello();
}
使用枚舉實現(xiàn)單例模式不僅防止了反射構建對象也保證了線程安全,但是同時它并不是懶加載,在枚舉類加載的同時,其單例對象就已經(jīng)被初始化。
總結(jié)
單例模式寫法總結(jié)起來可以分為五種懶漢、惡漢、雙重校驗鎖、枚舉、靜態(tài)內(nèi)部類,上述所說都是線程安全的實現(xiàn),第一種應該說是不正確的實現(xiàn)。
對于這幾種的比較
| 單例模式 | 是否線程安全 | 是否懶加載 | 是否防止反射構建 |
|---|---|---|---|
| 雙重校驗鎖 | 是 | 是 | 否 |
| 枚舉 | 是 | 否 | 是 |
| 靜態(tài)內(nèi)部類 | 是 | 是 | 否 |
補充
- volatile關鍵字不但可以防止指令重排,也可以保證線程訪問的變量值是主內(nèi)存中的最新值。有關volatile的詳細原理,我在以后的漫畫中會專門講解。
- 使用枚舉實現(xiàn)的單例模式,不但可以防止利用反射強行構建單例對象,而且可以在枚舉類對象被反序列化的時候,保證反序列的返回結(jié)果是同一對象。
對于其他方式實現(xiàn)的單例模式,如果既想要做到可序列化,又想要反序列化為同一對象,則必須實現(xiàn)readResolve方法。- 應該在任何情況下都應實現(xiàn)線程安全的寫法。