設計模式-GOF-創建型-Singleton單例模式

設計模式-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會進行指令重排
創建對象有四步:

  1. 分配對象的內存空間
  2. 初始化對象
  3. 執行構造器初始化
  4. 將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 效果時,纔會使用登記式/靜態內部類。如果涉及到反序列化創建對象時,可以嘗試使用枚舉方式。如果有其他特殊的需求,可以考慮使用雙檢鎖/雙重校驗鎖方式。

參考鏈接

https://mp.weixin.qq.com/s/2UYXNzgTCEZdEfuGIbcczA

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章