在我們日常的工作中經常需要在應用程序中保持一個唯一的實例,如:IO處理,數據庫操作等,由於這些對象都要占用重要的系統資源,所以我們必須限制這些實例的創建或始終使用一個公用的實例,這就是我們今天要介紹的——單例模式(Singleton)。
定義
單例模式,又稱單件模式或者單子模式,指的是一個類只有一個實例,並且提供一個全局訪問點。
實現思路
在單例的類中設置一個 private 靜態變量instance,instance 類型為當前類,用來持有單例唯一的實例。
將(無參數)構造器設置為 private,避免外部使用 new 構造多個實例。
提供一個 public 的靜態方法,如 getInstance,用來返回該類的唯一實例 instance。
類圖

幾種實現方式
由於使用場景不同,出現不同寫法和模式,它們分別:
-
懶漢式
-
惡漢式
-
雙重校驗鎖
-
枚舉
-
靜態內部類
由於枚舉使用場景場景較少, 下面就不介紹,感興趣的可以自行解決。
惡漢式
餓漢式指的是單例的實例在類裝載時進行創建。由於是在類裝時候創建, 所以能夠保證線程安全。如果單例類的構造方法中沒有包含過多的操作處理,餓漢式其實是可以接受的。
-
public class SingleInstance { -
private final static SingleInstance instance = new SingleInstance(); -
-
public static SingleInstance getInstance() { -
return instance; -
} -
}
不足:
-
如果構造方法中存在過多的處理,會導致加載這個類時比較慢,可能引起性能問題。
-
如果使用餓漢式的話,只進行了類的裝載,並沒有實質的調用,會造成資源的浪費。
懶漢式
懶漢式指的是單例實例在第一次使用時進行創建。這種情況下避免了上面餓漢式可能遇到的問題。
public class SingleInstance {
-
private static SingleInstance instance; -
private SingleInstance() { -
} -
-
public static SingleInstance getInstance() { -
if (null == instance) { -
instance = new SingleInstance(); -
} -
return instance; -
} -
}
但是如果上面的代碼在多線程並發的情況下就會發生問題, 因為它們存在共同的「臨界資源」 instance, 比如線程A進入 null == instance 這段代碼塊,而在A線程未創建完成實例時,這時線程B也進入了該代碼塊,必然會造成兩個實例的產生。
所以如果多線程這里要考慮加鎖同步。代碼實現如下:
-
public class SingleInstance { -
private static SingleInstance instance; -
private SingleInstance() { -
} -
-
public static synchronized SingleInstance getInstance() { -
if (null == instance) { -
instance = new SingleInstance(); -
} -
return instance; -
} -
}
如果使用 synchronized 修飾 getInstance 方法后必然會導致性能下降,而且 getInstance 是一個被頻繁調用的方法。雖然這種方法能解決問題,但是不推薦使用在多線程的情況下。所以偉大人類又想到了 「雙重檢查加鎖」。
雙重校驗鎖
偉大人類想到首先進入該方法時進行 null == sInstance 檢查,如果第一次檢查通過,即沒有實例創建,則進入 synchronized 控制的同步塊,並再次檢查實例是否創建,如果仍未創建,則創建該實例。
-
public class SingleInstance { -
private static SingleInstance instance; -
private SingleInstance() { -
} -
-
public static SingleInstance getInstance() { -
if (null == instance) { -
synchronized (SingleInstance.class) { -
if(null == instance) { -
instance = new SingleInstance(); -
} -
} -
} -
return instance; -
} -
}
雙重檢查加鎖保證了多線程下只創建一個實例,並且加鎖代碼塊只在實例創建的之前進行同步。如果實例已經創建后,進入該方法,則不會執行到同步塊的代碼。
靜態內部類
-
public class SingleInstance { -
private SingleInstance() { -
} -
-
public static SingleInstance getInstance() { -
return SingleInstanceHolder.sInstance; -
} -
-
private static class SingleInstanceHolder { -
private static SingleInstance sInstance = new SingleInstance(); -
} -
}
上面的代碼 Singleton 類被裝載了,instance 不一定被初始化。因為 SingletonHolder 類沒有被主動使用,只有顯示通過調用 getInstance 方法時,才會顯示裝載 SingletonHolder 類,從而實例化 instance。想象一下,如果實例化 instance 很消耗資源,我想讓它延遲加載, 上面就能這種方式就能達到。
優點:
-
單例模式(Singleton)會控制其實例對象的數量,從而確保訪問對象的唯一性。
-
實例控制:單例模式防止其它對象對自己的實例化,確保所有的對象都訪問一個實例。
-
伸縮性:因為由類自己來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。
缺點:
-
系統開銷。雖然這個系統開銷看起來很小,但是每次引用這個類實例的時候都要進行實例是否存在的檢查。這個問題可以通過靜態實例來解決。
-
使用多個類加載器加載單例類,也會導致創建多個實例並存的問題。
-
使用反射,雖然構造器為非公開,但是在反射面前就不起作用了。
-
對象生命周期。因為單例模式沒有提出對象的銷毀, 所以使用時容易造成內存泄漏, 例如在 Android 中在 Activity 中使用單例, 所以我們要額外小心。
使用場景
-
系統只需要一個實例對象,如系統要求提供一個唯一的序列號生成器,或者需要考慮資源消耗太大而只允許創建一個對象。
-
不要使用單例模式存取全局變量。這違背了單例模式的用意,最好放到對應類的靜態成員中。
-
在一個系統中要求一個類只有一個實例時才應當使用單例模式。反過來,如果一個類可以有幾個實例共存,就需要對單例模式進行改進,使之成為多例模式
Android 系統中的應用
在 Android 系統中, 大量使用單例模式, 我們來看一下。
惡漢式:
-
public class CallManager { -
... -
// Singleton instance -
private static final CallManager INSTANCE = new CallManager(); -
-
public static CallManager getInstance() { -
return INSTANCE; -
} -
.... -
}
懶漢式非線程安全實現方式:
-
class SnackbarManager { -
..... -
private static SnackbarManager sSnackbarManager; -
-
static SnackbarManager getInstance() { -
if (sSnackbarManager == null) { -
sSnackbarManager = new SnackbarManager(); -
} -
return sSnackbarManager; -
} -
}
懶漢式線程安全實現方式:
-
public class SystemConfig { -
... -
static SystemConfig sInstance; -
... -
public static SystemConfig getInstance() { -
synchronized (SystemConfig.class) { -
if (sInstance == null) { -
sInstance = new SystemConfig(); -
} -
return sInstance; -
} -
} -
}
總結
一般的情況下,構造方法沒有太多處理時,我會使用「惡漢」方式, 因為它簡單易懂,而且在JVM層實現了線程安全(如果不是多個類加載器環境)。只有在要明確實現延遲加載效果時我才會使用「靜態內部類」方式。
