經常使用設計模式之單例模式

單例對象(Singleton)是一種經常使用的設計模式。在Java應用中,單例對象能保證在一個JVM中,該對象只
有一個實例存在。這樣的模式有幾個好處:
一、某些類建立比較頻繁,對於一些大型的對象,這是一筆很大的系統開銷。
二、省去了new操做符,下降了系統內存的使用頻率,減輕GC壓力。
三、有些類如交易所的核心交易引擎,控制着交易流程,若是該類能夠建立多個的話,系統完
全亂了。設計模式

例子:
//簡單的單例類 餓漢模式
public class Singleton { 安全

  /* 持有私有靜態實例,防止被引用*/
  private static Singleton instance = new Singleton(); 多線程

  /* 私有構造方法,防止被實例化 */
  private Singleton() {
  } 函數

  /* 靜態工程方法,返回Singleton實例 */
  public static Singleton getInstance() {
    return instance;
  } 性能

  /* 若是該對象被用於序列化,能夠保證對象在序列化先後保持一致 */
  private Object readResolve() {
    return instance;
  }
} 測試

這個類是能夠實現單例模式的,可是存在很多問題,好比在類中無論用戶是否要使用該類的對象,就先建立
好了一個實例放在內存中。優化

//簡單的單例類 懶漢模式
public class Singleton { 線程

  /* 持有私有靜態實例,防止被引用,此處賦值爲null,目的是實現延遲加載 */
  private static Singleton instance = null; 設計

  /* 私有構造方法,防止被實例化 */
  private Singleton() {
  } 對象

  /* 靜態工程方法,建立實例 */
  public static Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  }

  /* 若是該對象被用於序列化,能夠保證對象在序列化先後保持一致 */
  private Object readResolve() {
    return instance;
  }
}

這個類能夠知足基本要求,可是,像這樣毫無線程安全保護的類,若是咱們把它放入多線程的環境下,確定
就會出現問題了,如何解決?咱們首先會想到對getInstance方法加synchronized關鍵字,以下:

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

可是,synchronized做爲修飾符在方法上使用,在性能上會有所降低,由於每次調用getInstance(),都要對
對象上鎖,事實上,只有在第一次建立對象的時候須要加鎖,以後就不須要了,因此,這個地方須要改進。
咱們改爲下面這個:
public static Singleton getInstance() {
  if (instance == null) {
    synchronized (instance) {
      if (instance == null) {
        instance = new Singleton();
      }
    }
  }
  return instance;
}

彷佛解決了以前提到的問題,將synchronized關鍵字加在了方法內部,也就是說當調用的時候是不須要加鎖
的,只有在instance爲null,並建立對象的時候才須要加鎖,性能有必定的提高。可是,這樣的狀況,仍是
有可能有問題的。
看下面的狀況:在Java指令中建立對象和賦值操做是分開進行的,也就是說instance = new Singleton();
語句並不是是一個原子操做,在 JVM 中這句代碼大概作了下面 3 件事情:
  1給 new的對象 分配內存
  2調用 Singleton 的構造函數來初始化成員變量
  3將引用instance指向分配的內存空間(執行完這步 instance 就爲非 null 了)
可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,另一個線程B搶奪到了CPU的執行權,這時instance已是非null了(但卻沒有初始化),因此線程B會直接返回 instance,而後使用,結果就會出現問題了(由於對象尚未初始化)。

還有另一種解決方案:使用內部類來維護單例的實現,JVM內部的機制可以保證當一個類被加載的時候,這
個類的加載過程是線程互斥的(就是加載完畢後別的線程才能使用)。這樣當咱們第一次調用getInstance的
時候,JVM可以幫咱們保證instance只被建立一次,而且會保證把賦值給instance的內存初始化完畢,這樣
咱們就不用擔憂上面的問題。同時該方法也只會在第一次調用的時候使用互斥機制,這樣就解決了低性能
問題。例如:

public class Singleton {

  /* 私有構造方法,防止被實例化 */
  private Singleton() {
  }

  /* 此處使用一個內部類來維護單例 */
  private static class SingletonFactory {
    private static Singleton instance = new Singleton();
  }

  /* 獲取實例 */
  public static Singleton getInstance() {
    return SingletonFactory.instance;
  }

  /* 若是該對象被用於序列化,能夠保證對象在序列化先後保持一致 */
  private Object readResolve() {
    return getInstance();
  }
}

可是若是在構造函數中拋出異常,實例將永遠得不到建立,也會出錯。因此說,十分完美的東西是沒有的,
咱們只能根據實際狀況,選擇最適合本身應用場景的實現方法。

同時,咱們還可使用反射去建立這個類的對象,即便它的構造器是私有的,咱們也是能夠調用到的。那
麼這個時候咱們就須要再次修改代碼去訪問別人反射調用構造器。
例子:
//在構造器中控制一下,構造器只容許調用一次,以後在調用就拋出異常
public class Singleton {
  private volatile static boolean flag;
  /* 私有構造方法,防止被實例化 */
  private Singleton() {
    if(!flag){
      flag = false;
    }else{
      throw new RuntimeException("不能屢次建立單例對象");
    }
  }

  /* 此處使用一個內部類來維護單例 */
  private static class SingletonFactory {
    private static Singleton instance = new Singleton();
  }

  /* 獲取實例 */
  public static Singleton getInstance() {
    return SingletonFactory.instance;
  }

  /* 若是該對象被用於序列化,能夠保證對象在序列化先後保持一致 */
  private Object readResolve() {
    return getInstance();
  }
}

反射的問題處理完了以後,這裏還有一個問題,就是若是把單例對象進行序列化而後再反序列化,那麼內存
中就會出現倆個同樣的單例對象,只是內存地址不一樣。這種狀況咱們可使用readResolve方法來防止。
private Object readResolve(){.....}
ObjectInputStream 會檢查對象的class是否認義了readResolve方法。若是定義了,將由readResolve方法
指定返回的對象。返回對象的類型必定要是兼容的,不然會拋出ClassCastException 。
例子:
public abstract class Singleton8 implements Serializable{

  private static final long serialVersionUID = 7863921642928237696L;

  /* 此處使用一個內部類來維護單例 */
  private static class SingletonFactory {
    private static Singleton8 instance = new Singleton8(){};
  }

  //測試方式,把單例對象序列化後再反序列化從而得到一個新的對象 就至關於複製了一個單例對象
  public Singleton8 copy() throws Exception{
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(os);
    oos.writeObject(Singleton8.getInstance());
    InputStream is = new ByteArrayInputStream(os.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(is);
    Singleton8 obj = (Singleton8) ois.readObject();
    return obj;
  }

  /* 獲取實例 */
  public static Singleton8 getInstance() {
    return SingletonFactory.instance;
  }

  /* 若是該對象被用於序列化,能夠保證對象在序列化先後保持一致 */
  /* 把這個方法註釋前和註釋後來運行測試代碼觀察結果 */
  private Object readResolve() {
    return getInstance();
  }
}

實現和完善單例模式的方法有不少,筆者就不一一列舉了。

相關文章
相關標籤/搜索