設計模式-GOF-創建型-Singleton單例模式
定義
指一個類只有一個實例,且該類能自行創建這個實例的一種模式。
主要解決: 一個全局使用的類頻繁地創建與銷燬
優點:
- 在內存中只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷燬實例
- 避免對資源的多重佔用
特點:
- 單例類只有一個實例對象
- 該單例對象必須由單例類自行創建
- 單例類對外提供一個訪問該單例的全局訪問點
使用場景
- 創建一個對象需要消耗的資源過多,比如I/O與數據庫的連接
- 爲控制實例數量,節省系統資源
- 全局數據共享
實現
- 私有化構造器,以防外部類來通過new生成實例
- 定義一個靜態私有實例,並對外提供一個靜態方法用於創建或獲取靜態私有實例
幾種不同的實現方式
實現方式 | 是否線程安全 | 是否懶加載 | 是否防止反射構建 | 是否使用類加載機制 |
---|---|---|---|---|
餓漢式 | 是 | 否 | 否 | 是 |
雙重校驗鎖 | 是 | 是 | 否 | 否 |
靜態內部類 | 是 | 是 | 否 | 是 |
枚舉 | 是 | 否 | 是 | ? |
枚舉與類加載機制是否有關還沒理解透
懶漢式,線程不安全
是否Lazy初始化:是
描述:實現最簡單但不支持多線程。
public class SingleObject {
private static SingleObject instance;
private SingleObject() {}
public static SingleObject getInstance() {
if(instance==null) {
instance = new SingleObject();
}
return instance;
}
}
懶漢式,線程安全
是否Lazy初始化:是
描述:這種方式具備很好的Lazy loading,能保證多線程安全,但效率低,99%的情況下不需要同步。
優點:第一次調用才初始化,避免內存浪費
缺點:加鎖影響效率
public class SingleObject {
private static SingleObject instance;
private SingleObject() {}
public static synchronized SingleObject getInstance() {
if(instance==null) {
instance = new SingleObject();
}
return instance;
}
}
餓漢式(類加載時創建)
是否Lazy初始化:否
是否多線程安全:是
描述:比較常用,但容易產生垃圾對象
優點:沒有鎖,效率高
缺點:類加載時就初始化,浪費內存
它基於classloader機制避免了多線程同步問題,但沒有達到lazy loading的效果
public class SingleObject {
private static SingleObject instance = new SingleObject();
private SingleObject() {}
public static SingleObject getInstance() {
return instance;
}
}
雙檢鎖/雙重校驗鎖(DCL,即double-checked locking)
JDK版本:JDK1.5起
是否Lazy初始化:是
是否多線程安全:是?
描述:這種方式採用雙鎖機制,安全且在多線程情況下能保持高性能。getInstance() 的性能對應用程序很關鍵。這種方法還有漏洞不能保證絕對的線程安全
public class SingleObject {
private static SingleObject instance;
private SingleObject() {}
public static SingleObject getInstance() {
if(instance==null) {
synchronized (SingleObject.class) {
if(instance==null) {
instance = new SingleObject();
}
}
}
return instance;
}
}
多線程不絕對安全的原因:
JVM會進行指令重排
創建對象有四步:
- 分配對象的內存空間
- 初始化對象
- 執行構造器初始化
- 將instance指向剛分配的內存地址(剛創建的對象)
經過優化順序可能發生改變,可能導致A線程對象創建還沒有完成,但B線程判斷instance時instance已經不爲null並且返回了一個沒有初始化完成的instance對象。
改進,添加volatile
volatile 防止JVM進行指令重排
public class SingleObject {
private volatile static SingleObject instance;
private SingleObject() {}
public static SingleObject getInstance() {
if(instance==null) {
synchronized (SingleObject.class) {
if(instance==null) {
instance = new SingleObject();
}
}
}
return instance;
}
}
登記式/靜態內部類
是否Lazy初始化:是
是否多線程安全:是
描述:對靜態域使用延遲初始化。基於classloader類加載機制實現,在通過顯式調用getInstance()時纔會加載內部類,從而到達Lazy loading的目的。
注意:內部靜態類無法從外部訪問
public class SingleObject {
private static class LazyHolder{
private static final SingleObject INSTANCE = new SingleObject();
}
private SingleObject() {}
public static SingleObject getInstance() {
return LazyHolder.INSTANCE;
}
}
以上方法均可用反射打破單例
方法:
//獲得構造器
Constructor con = SingleObject.class.getDeclaredConstructor();
//設置爲可訪問
con.setAccessible(true);
//構造兩個不同的對象
SingleObject single1 = (SingleObject)con.newInstance();
SingleObject single2 = (SingleObject)con.newInstance();
//驗證是否是不同對象
System.out.println(single1.equals(single2));
代碼可以簡單歸納爲三個步驟:
第一步,獲得單例類的構造器。
第二步,把構造器設置爲可訪問。
第三步,使用newInstance方法構造對象。
最後爲了確認這兩個對象是否真的是不同的對象,我們使用equals方法進行比較。毫無疑問,比較結果是false。
枚舉
JDK版本:JDK1.5起
是否Lazy初始化:否
比較神奇簡潔
描述:這種實現方式還沒有被廣泛採用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止多次實例化。
這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化。不過,由於 JDK1.5 之後才加入 enum 特性,用這種方式寫不免讓人感覺生疏,在實際工作中,也很少用。
不能通過 reflection attack 來調用私有構造方法。
public enum SingleObject {
INSTANCE;
}
具體原理還需研究
備註:
- volatile關鍵字不但可以防止指令重排,也可以保證線程訪問的變量值是主內存中的最新值。
- 使用枚舉實現的單例模式,不但可以防止利用反射強行構建單例對象,而且可以在枚舉類對象被反序列化的時候,保證反序列的返回結果是同一對象。
- 對於其他方式實現的單例模式,如果既想要做到可序列化,又想要反序列化爲同一對象,則必須實現readResolve方法。
private Object readResolve() throws ObjectStreamException{
return instance;
}
最後
經驗之談(這是我copy來的,具體還是看個人需求吧):一般情況下,不建議使用懶漢式線程不安全和懶漢式線程安全,建議使用餓漢式。只有在要明確實現 lazy loading 效果時,纔會使用登記式/靜態內部類。如果涉及到反序列化創建對象時,可以嘗試使用枚舉方式。如果有其他特殊的需求,可以考慮使用雙檢鎖/雙重校驗鎖方式。
參考鏈接