單例模式(Singleton pattern)

爲什麼要使用單例模式

單例模式屬於上篇說過的設計模式三大分類中的第一類——創建型模式。顧名思義,單例設計模式就是爲了保證創建出來的對象實例只有一個。

  • 通過控制創建對象的數量,節約系統資源開銷。
  • 有些場景下,不使用單例模式,會導致系統同一時刻出現多個狀態缺乏同步,用戶自然無法判斷當前處於什麼狀態
  • 全局數據共享

餓漢式

餓漢式:在類加載的時候完成初始化,不管是否使用該單例對象,先初始化出來再說。
缺點:可能不使用初始化單例對象,但調用類其他靜態成員是,類加載就完成了初始化,這是不必要的開銷,如果單例對象的初始化非常複雜,就會造成資源的浪費。

public class HungrySingleton {
  private HungrySingleton() {}  // 構造方法私有化,防止通過new創建對象實例

  private static HungrySingleton instance = new HungrySingleton();  //持有私有static修飾的對象實例引用,

  public static HungrySingleton getInstance() {     //公開靜態獲取對象實例的方法
    return instance;
  }
}

懶漢式

懶漢式:在需要用到單例對象實例的時候才完成初始化工作,能不初始化就不初始化。
實現的關鍵是:定義單例對象的引用時,沒有初始化,在獲取實例對象的方法中判定是否完成初始化,沒有初始化才進行單例對象的創建工作。

public class LazySingleton {
  private static LazySingleton instance = null; //靜態私有空對象引用
  private LazySingleton() {  //構造函數私有化
  }
  public static LazySingleton getInstance() { //公開獲取實例方法
    if(instance == null){   //對象爲空時,創建對象實例
      instance = new LazySingleton();
    }
    return instance;
  }
}

思考:這段程序看起來沒什麼問題,但是如果在高併發的環境下,就可能出現問題,因爲可能有多個線程同時執行到判空操作,都通過了空指針的條件,進行對象的創建,解決方法就是對創建對象的過程進行同步,引入同步代碼塊。

public class LazySingleton {
  private LazySingleton() {
  }
  private static volatile LazySingleton instance = null;

  public static synchronized LazySingleton getInstance() {
    if(instance == null){
      instance = new LazySingleton();
    }
    return instance;
  }
}

這樣加同步鎖,確實也能解決了高併發的問題了,但是,鎖的粒度是否有點過大呢?如果對象已經創建,還是需要排隊進入同步代碼塊,效率有點低下了,其實可以直接返回實例,優化代碼如下,傳說中的雙重檢測鎖

public class LazySingleton {
  private LazySingleton() {
  }
  private static volatile LazySingleton instance = null;    //注意要用volatile修飾,保證線程可見性

  public static LazySingleton getInstance() {
    if(instance == null){
      synchronized(LazySingleton.class){
        if(instance == null)
          instance = new LazySingleton();
      }
    }
    return instance;
  }
}

單例模式的其他實現方式

靜態內部類實現

根據類加載機制,外部類的初始化並不會導致靜態內部類的初始化。

public class StaticInnerSingleton {
    private StaticInnerSingleton() {
    }
    private static class StaticInnerSingletonInstance {
      private static final StaticInnerSingleton instance = new StaticInnerSingleton();
    }
    public static StaticInnerSingleton getInstance() {
      return StaticInnerSingletonInstance.instance;
    }
}
  1. StaticInnerSingletonInstance是一個靜態內部類,內部靜態字段instance負責創建對象。因爲上面的結論,所以外部類StaticInnerSingleton初始化時,並不會導致StaticInnerSingletonInstance初始化,進而導致instance的初始化。所以實現了延遲加載。
  2. 當外部調用getInstance()時,通過StaticInnerSingletonInstance.instance對instance引用纔會導致對象的創建。由於static的屬性只會跟隨類加載初始化一次,天然保證了線程安全問題。

枚舉實現

用枚舉實現單例是最簡單的了,因爲,Java中的枚舉類型本身就天然單例的

enum EnumSingletonInstance{
   INSTANCE;
   public static EnumSingletonInstance getInstance(){
   		return INSTANCE;
   }
}

唯一遺憾的是,這個方案和餓漢式一樣,沒法延遲加載。枚舉類加載自然就會初始化INSTANCE。

破解單例

反射破解法

public static void main(String[] args) throws Exception {
        System.out.println(HungrySingleton.getInstance());
        System.out.println(HungrySingleton.getInstance());
        System.out.println("反射破解單例...");
        HungrySingleton instance1 = HungrySingleton.class.newInstance();
        HungrySingleton instance2 = HungrySingleton.class.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
}

反射的原理是調用默認的無參構造函數進行初始化,防止反射破解單例的方法就是在無參構造函數加入單例對象引用是否初始化的判斷,如果已經初始化,拋出異常

private HungrySingleton() {
    if (instance != null) {
      try {
        throw new Exception("只能創建一個對象!");
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
}

反序列化破解法

通過序列化和反序列化也可以破解單例。(前提是單例類實現了Serializable接口)代碼如下:

public static void main(String[] args) throws Exception {
        System.out.println(HungrySingleton.getInstance());
        System.out.println(HungrySingleton.getInstance());
        System.out.println("反序列化破解單例...");
        HungrySingleton instance1 = HungrySingleton.getInstance();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(baos);
        out.writeObject(instance1);	//序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        HungrySingleton instance2 = (HungrySingleton) ois.readObject();	//反序列化
        System.out.println(instance1);
        System.out.println(instance2);
}

要防止序列化破壞單例,只需要在單例類中添加如下readResolve()方法,然後在方法體中返回我們的單例實例即可。爲什麼?因爲readResolve()方法是在readObject()方法之後才被調用,因而它每次都會用我們自己生成的單實例替換從流中讀取的對象。這樣自然就保證了單例。

private Object readResolve() throws ObjectStreamException{
  return instance;
}

總結

從安全性角度考慮,枚舉顯然是最安全的,保證絕對的單例,因爲可以天然防止反射和反序列化的破解手段。而其它方案一定場合下全部可以被破解。

從延遲加載考慮,懶漢式、雙重檢測鎖、靜態內部類方案都可以實現,然而雙重檢測鎖方案代碼實現複雜,而且還有對JDK版本的要求,首先排除。懶漢式加鎖性能較差,
而靜態內部類實現方法既能夠延遲加載節約資源,另外也不需要加鎖,性能較好,所以這方面考慮靜態內部類方案最佳。

餓漢式和懶漢式實現有幾個要點:
1. 構造函數私有化
2. private static修飾的單例對象引用
3. 公開的獲取對象實例的靜態方法

注:本文參考 java進階架構師 文章,文章地址:

發佈了20 篇原創文章 · 獲贊 3 · 訪問量 5823
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章