單例模式有幾種寫法?

世上沒有白費的努力,也沒有碰巧的成功,全部的無意插柳,最後都會是水到渠成。面試

「你知道茴香豆的‘茴’字有幾種寫法嗎?」
糾結單例模式有幾種寫法有用嗎?有點用,面試中常常選擇其中一種或幾種寫法做爲話頭,考查設計模式和代碼風格的同時,還很容易擴展到其餘問題。
這裏講解幾種經常使用的寫法,但切忌生搬硬套。大致可分爲4類,下面分別介紹他們的基本形式、變種及特色。設計模式

飽漢模式

飽漢是變種最多的單例模式。咱們從飽漢出發,經過其變種逐漸瞭解實現單例模式時須要關注的問題。安全

基礎的飽漢

飽漢,即已經吃飽,不着急再吃,餓的時候再吃。因此他就先不初始化單例,等第一次使用的時候再初始化,即「懶加載」。微信

// 飽漢
// UnThreadSafe
public class Singleton1
{
    private static Singleton1 singleton = null;
    private Singleton1()
    {}
    public static Singleton1 getInstance()
    {
        if(singleton == null)
        {
            singleton = new Singleton1();
        }
        return singleton;
    }
}

飽漢模式的核心就是懶加載。好處是更啓動速度快、節省資源,一直到實例被第一次訪問,才須要初始化單例;小壞處是寫起來麻煩,大壞處是線程不安全,if語句存在競態條件。多線程

寫起來麻煩不是大問題,可讀性好啊。所以,單線程環境下,基礎飽漢是筆者最喜歡的寫法。但多線程環境下,基礎飽漢就完全不可用了。下面的幾種變種都在試圖解決基礎飽漢線程不安全的問題。併發

飽漢 - 變種 1

最粗暴的犯法是用synchronized關鍵字修飾getInstance()方法,這樣能達到絕對的線程安全。性能

// 飽漢
// ThreadSafe
public class Singleton1_1
{
    private static Singleton1_1 singleton = null;
    private Singleton1_1()
    {}
    public synchronized static Singleton1_1 getInstance()
    {
        if(singleton == null)
        {
            singleton = new Singleton1_1();
        }
        return singleton;
    }
}

變種1的好處是寫起來簡單,且絕對線程安全;壞處是併發性能極差,事實上徹底退化到了串行。單例只須要初始化一次,但就算初始化之後,synchronized的鎖也沒法避開,從而getInstance()徹底變成了串行操做。性能不敏感的場景建議使用。flex

飽漢 - 變種 2

變種2是「臭名昭著」的DCL 1.0。this

針對變種1中單例初始化後鎖仍然沒法避開的問題,變種2在變種1的外層又套了一層check,加上synchronized內層的check,即所謂「雙重檢查鎖」(Double Check Lock,簡稱DCL)。spa

// 飽漢
// UnThreadSafe
public class Singleton1_2
{
    private static Singleton1_2 singleton = null;
    public int f1 = 1; // 觸發部分初始化問題
    public int f2 = 2;
    private Singleton1_2()
    {}
    public static Singleton1_2 getInstance()
    {
        // may get half object
        if(singleton == null)
        {
            synchronized(Singleton1_2.class)
            {
                if(singleton == null)
                {
                    singleton = new Singleton1_2();
                }
            }
        }
        return singleton;
    }
}

變種2的核心是DCL,看起來變種2彷佛已經達到了理想的效果:懶加載+線程安全。惋惜的是,正如註釋中所說,DCL仍然是線程不安全的,因爲指令重排序,你可能會獲得「半個對象」,即」部分初始化「問題。

飽漢 - 變種 3

變種3專門針對變種2,可謂DCL 2.0。

針對變種3的「半個對象」問題,變種3在instance上增長了volatile關鍵字,原理見上述參考。

// 飽漢
// ThreadSafe
public class Singleton1_3
{
    private static volatile Singleton1_3 singleton = null;
    public int f1 = 1; // 觸發部分初始化問題
    public int f2 = 2;
    private Singleton1_3()
    {}
    public static Singleton1_3 getInstance()
    {
        if(singleton == null)
        {
            synchronized(Singleton1_3.class)
            {
                // must be a complete instance
                if(singleton == null)
                {
                    singleton = new Singleton1_3();
                }
            }
        }
        return singleton;
    }
}

多線程環境下,變種3更適用於性能敏感的場景。但後面咱們將瞭解到,就算是線程安全的,還有一些辦法能破壞單例。

固然,還有不少方式,能經過與volatile相似的方式防止部分初始化。讀者可自行閱讀內存屏障相關內容,但面試時不建議主動裝逼。

餓漢模式

與飽漢相對,餓漢很餓,只想着儘早吃到。因此他就在最先的時機,即類加載時初始化單例,之後訪問時直接返回便可。

// 餓漢
// ThreadSafe
public class Singleton2
{
    private static final Singleton2 singleton = new Singleton2();
    private Singleton2()
    {}
    public static Singleton2 getInstance()
    {
        return singleton;
    }
}

餓漢的好處是天生的線程安全(得益於類加載機制),寫起來超級簡單,使用時沒有延遲;壞處是有可能形成資源浪費(若是類加載後就一直不使用單例的話)。

值得注意的時,單線程環境下,餓漢與飽漢在性能上沒什麼差異;但多線程環境下,因爲飽漢須要加鎖,餓漢的性能反而更優。

Holder模式

咱們既但願利用餓漢模式中靜態變量的方便和線程安全;又但願經過懶加載規避資源浪費。Holder模式知足了這兩點要求:核心仍然是靜態變量,足夠方便和線程安全;經過靜態的Holder類持有真正實例,間接實現了懶加載。

// Holder模式
// ThreadSafe
public class Singleton3
{
    private static class SingletonHolder
    {
        private static final Singleton3 singleton = new Singleton3();
        private SingletonHolder()
        {}
    }
    private Singleton3()
        {}
        /**
        * 勘誤:多寫了個synchronized。。
        public synchronized static Singleton3 getInstance() {
          return SingletonHolder.singleton;
        }
        */
    public static Singleton3 getInstance()
    {
        return SingletonHolder.singleton;
    }
}

相對於餓漢模式,Holder模式僅增長了一個靜態內部類的成本,與飽漢的變種3效果至關(略優),都是比較受歡迎的實現方式。一樣建議考慮。

枚舉模式

用枚舉實現單例模式,至關好用,但可讀性是不存在的。

基礎的枚舉

將枚舉的靜態成員變量做爲單例的實例:

// 枚舉
// ThreadSafe
public enum Singleton4
{
    SINGLETON;
}

代碼量比餓漢模式更少。但用戶只能直接訪問實例Singleton4.SINGLETON——事實上,這樣的訪問方式做爲單例使用也是恰當的,只是犧牲了靜態工廠方法的優勢,如沒法實現懶加載。

醜陋但好用的語法糖

Java的枚舉是一個「醜陋但好用的語法糖」。

枚舉型單例模式的本質

經過反編譯打開語法糖,就看到了枚舉類型的本質,簡化以下:

// 枚舉
// ThreadSafe
public class Singleton4 extends Enum < Singleton4 >
{...
    public static final Singleton4 SINGLETON = new Singleton4();...
}

本質上和餓漢模式相同,區別僅在於公有的靜態成員變量。

用枚舉實現一些trick

這一部分與單例沒什麼關係,能夠跳過。若是選擇閱讀也請認清這樣的事實:雖然枚舉至關靈活,但如何恰當的使用枚舉有必定難度。一個足夠簡單的典型例子是TimeUnit類,建議有時間耐心閱讀。

上面已經看到,枚舉型單例的本質仍然是一個普通的類。實際上,咱們能夠在枚舉型型單例上增長任何普通類能夠完成的功能。要點在於枚舉實例的初始化,能夠理解爲實例化了一個匿名內部類。爲了更明顯,咱們在Singleton4_1中定義一個普通的私有成員變量,一個普通的公有成員方法,和一個公有的抽象成員方法,以下:

// 枚舉
// ThreadSafe
public enum Singleton4_1
{
    SINGLETON("enum is the easiest singleton pattern, but not the most readable")
    {
        public void testAbsMethod()
        {
            print();
            System.out.println("enum is ugly, but so flexible to make lots of trick");
        }
    };
    private String comment = null;
    Singleton4_1(String comment)
    {
        this.comment = comment;
    }
    public void print()
    {
        System.out.println("comment=" + comment);
    }
    abstract public void testAbsMethod();
    public static Singleton4_1 getInstance()
    {
        return SINGLETON;
    }
}

這樣,枚舉類Singleton4_1中的每個枚舉實例不只繼承了父類Singleton4_1的成員方法print(),還必須實現父類Singleton4_1的抽象成員方法testAbsMethod()。

總結

上面的分析都忽略了反射和序列化的問題。經過反射或序列化,咱們仍然可以訪問到私有構造器,建立新的實例破壞單例模式。此時,只有枚舉模式能自然防範這一問題。反射和序列化筆者還不太瞭解,但基本原理並不難,能夠在其餘模式上手動實現。

下面繼續忽略反射和序列化的問題,作個總結回味一下:
微信圖片_20200619151906.png

相關文章
相關標籤/搜索