單例模式的N種寫法

1.前言

寫完這個題目,我感覺自己好像"孔乙己"啊,回字的四種寫法要不要學啊~

我們經常會用到單例模式,但是我對他一直沒有一個統一的的認識,比如我清楚好多種單例的寫法,但是每一種是怎麼演化來的?具體解決了什麼問題?這塊就沒有那麼清晰了,因此此文對單例模式進行一個總結,同時手擼一下代碼加深理解.

2.介紹

單例模式,即某一個類在整個系統中有且僅有一個實例.

經常用來讀取配置,獲取連接等等.

3.實現思路

1.構造方法私有化.
2.提供靜態的方法,返回唯一實例.

這塊很好理解,要想保證只有唯一實例,構造方法就不能被別人調用,只能自己調用用來創建唯一的實例,同時,將構造方法私有化了,就需要對外提供一個訪問點,以方便其他類獲取這個實例.

4.具體實現

4.1 餓漢式

這種寫法的優勢就是,真的簡單,基本就是的實現思路的耿直實現,代碼如下:

public class HungrySingleton {
  private static HungrySingleton hungrySingleton = new HungrySingleton();
  private HungrySingleton() {
  }
  public static HungrySingleton getSingleton() {
    return hungrySingleton;
  }
}

這樣子有個問題,就是隻要這個類被加載了,那麼就會創建出唯一實例,也不管用不用...

雖然其實工作中問題不大,但是學習嘛,就要吹毛求疵,我們要懶加載的方式!

4.2 懶漢式

代碼如下:

public class LazySingleton {

  private static LazySingleton lazySingleton = null;

  private LazySingleton() {
  }

  public static LazySingleton getSingleton() {
    if (null == lazySingleton) {
      lazySingleton = new LazySingleton();
    }
    return lazySingleton;
  }
}

這種方式也挺好理解的,而且實現了懶加載!只有在調用的時候才創建實例,節省了好大的空間呢!(並不)

但是這種方式仍然是有問題的,那就是著名的你有現在問題兩個了.

如果多個線程同時來請求獲取實例,上面這種懶漢式是解決不了的,會提供多個實例,也就違背了單例模式的初衷了(多個線程同時進入判空語句).

4.3 的懶漢優化一下

不就是線程安全嗎?把我知道的volatile和synchronized都用上!

public class LazySingleton2 {

  private static volatile LazySingleton2 lazySingleton = null;

  private LazySingleton2() {
  }

  public static LazySingleton2 getSingleton() {
    synchronized (LazySingleton2.class) {
      if (null == lazySingleton) {
        lazySingleton = new LazySingleton2();
      }
    }
    return lazySingleton;
  }

}

這種方法看起來沒有問題了,用volatilew修飾了唯一實例,保證內存可見性,用synchronized加鎖,每次只允許一個線程訪問判空語句,這不就解決了上面的問題嗎?

是的,殺雞用牛刀也不一定做的好啊..想想判空語句以及裏面的實例化的執行頻率,從理想的情況來講,只有第一次會執行創建實例,剩下的都是返回實例就完事了.

爲了這一種情況,每次都加鎖,,性能下降太厲害了(其實並不,加了鎖我們大部分時間也是夠用的).

那再優化一下.

4.4 雙重檢查鎖

代碼如下:

public class DoubleCheckSingleton {

  private static volatile DoubleCheckSingleton singleton = null;

  private DoubleCheckSingleton() {
  }

  public static DoubleCheckSingleton getSingleton() {
    if (null == singleton) {
      synchronized (DoubleCheckSingleton.class) {
        if (null == singleton) {
          singleton = new DoubleCheckSingleton();
        }
      }
    }
    //2
    return singleton;
  }

}

這就是傳說中的雙重檢查鎖了,說實話,這個代碼看起來我覺得有點難看....

但是其實是比較好使的,大部分的獲取實例請求都會直接來到//2位置,而極少量的爲空進行加鎖,保證線程安全.

雙重檢查鎖對上一步的優化是:多添加一重判斷,過濾掉大部分不需要加鎖的操作,同時,加鎖後再次進行判斷,防止在第一次判斷-加鎖期間已經創建了實例.

4.5 靜態內部類實現

public class InnerClassSingleton {

  private static class Holder {

    private static InnerClassSingleton singleton = new InnerClassSingleton();
  }

  private InnerClassSingleton() {
  }

  public static InnerClassSingleton getSingleton() {
    return Holder.singleton;
  }

}

我們可以把Singleton實例放到一個靜態內部類中,這樣就避免了靜態實例在Singleton類加載的時候就創建對象,並且由於靜態內部類只會被加載一次,所以這種寫法也是線程安全的:

4.6 枚舉寫法

上面的所有實現都有一點小問題:

  1. 序列化與反序列化沒有考慮,每次反序列化都能拿到一個新的實例.
  2. 反射,都可以通過反射強行調用privite的構造方法.

這時候就是枚舉類出現的時候了!

public enum EnumSingleton {
  SINGLETON;
}

在《Effective Java》最後推薦了這樣一個寫法,看起來簡直簡單的有點不可思議,那麼它是怎麼保證以上幾點的呢?

  1. 枚舉類的初始化過程天然線程安全.即保證了線程安全.
  2. 對枚舉的序列化與反序列禁止了自定義,由JDK實現,不會出現反序列化多個實例的情況.

在 《Effctive Java》中,作者極力推薦枚舉實現單例,甚至說了它是單例實現的最好寫法.

雖然我還沒有應用過枚舉實現單例,但是很快我就會將它加進我的代碼庫裏.

總結

在單例實現中,我們需要注意以下三個問題:

  1. (重要)延遲加載,避免浪費.
  2. (重要)線程安全,避免多個實例.
  3. 序列化安全.

完。









ChangeLog


2019-01-31 完成



以上皆爲個人所思所得,如有錯誤歡迎評論區指正。

歡迎轉載,煩請署名並保留原文鏈接。

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