爲何我牆裂建議你們使用枚舉來實現單例。

關於單例模式,個人博客中有不少文章介紹過。做爲23種設計模式中最爲經常使用的設計模式,單例模式並無想象的那麼簡單。由於在設計單例的時候要考慮不少問題,好比線程安全問題、序列化對單例的破壞等。html

單例相關文章一覽:java

設計模式(二)——單例模式設計模式

設計模式(三)——JDK中的那些單例安全

單例模式的七種寫法併發

單例與序列化的那些事兒oracle

不使用synchronized和lock,如何實現一個線程安全的單例?函數

不使用synchronized和lock,如何實現一個線程安全的單例?(二)spa

若是你對單例不是很瞭解,或者對於單例的線程安全問題以及序列化會破壞單例等問題不是很清楚,能夠先閱讀以上文章。上面六篇文章看完以後,相信你必定會對單例模式有更多,更深刻的理解。線程

咱們知道,單例模式,通常有七種寫法,那麼這七種寫法中,最好的是哪種呢?爲何呢?本文就來抽絲剝繭一下。設計

哪一種寫單例的方式最好

在StakcOverflow中,有一個關於What is an efficient way to implement a singleton pattern in Java?的討論:

單例

如上圖,得票率最高的回答是:使用枚舉。

回答者引用了Joshua Bloch大神在《Effective Java》中明確表達過的觀點:

使用枚舉實現單例的方法雖然尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。

若是你真的深刻理解了單例的用法以及一些可能存在的坑的話,那麼你也許也能獲得相同的結論,那就是:使用枚舉實現單例是一種很好的方法。

枚舉單例寫法簡單

若是你看過《單例模式的七種寫法》中的實現單例的全部方式的代碼,那就會發現,各類方式實現單例的代碼都比較複雜。主要緣由是在考慮線程安全問題。

咱們簡單對比下「雙重校驗鎖」方式和枚舉方式實現單例的代碼。

「雙重校驗鎖」實現單例:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  
複製代碼

枚舉實現單例:

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  
複製代碼

相比之下,你就會發現,枚舉實現單例的代碼會精簡不少。

上面的雙重鎖校驗的代碼之因此很臃腫,是由於大部分代碼都是在保證線程安全。爲了在保證線程安全和鎖粒度之間作權衡,代碼不免會寫的複雜些。可是,這段代碼仍是有問題的,由於他沒法解決反序列化會破壞單例的問題。

枚舉可解決線程安全問題

上面提到過。使用非枚舉的方式實現單例,都要本身來保證線程安全,因此,這就致使其餘方法必然是比較臃腫的。那麼,爲何使用枚舉就不須要解決線程安全問題呢?

其實,並非使用枚舉就不須要保證線程安全,只不過線程安全的保證不須要咱們關心而已。也就是說,其實在「底層」仍是作了線程安全方面的保證的。

那麼,「底層」到底指的是什麼?

這就要說到關於枚舉的實現了。這部份內容能夠參考個人另一篇博文深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題,這裏我簡單說明一下:

定義枚舉時使用enum和class同樣,是Java中的一個關鍵字。就像class對應用一個Class類同樣,enum也對應有一個Enum類。

經過將定義好的枚舉反編譯,咱們就能發現,其實枚舉在通過javac的編譯以後,會被轉換成形如public final class T extends Enum的定義。

並且,枚舉中的各個枚舉項同事經過static來定義的。如:

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}
複製代碼

反編譯後代碼爲:

public final class T extends Enum
{
    //省略部份內容
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}
複製代碼

瞭解JVM的類加載機制的朋友應該對這部分比較清楚。static類型的屬性會在類被加載以後被初始化,咱們在深度分析Java的ClassLoader機制(源碼級別)Java類的加載、連接和初始化兩個文章中分別介紹過,當一個Java類第一次被真正使用到的時候靜態資源被初始化、Java類的加載和初始化過程都是線程安全的(由於虛擬機在加載枚舉的類的時候,會使用ClassLoader的loadClass方法,而這個方法使用同步代碼塊保證了線程安全)。因此,建立一個enum類型是線程安全的。

也就是說,咱們定義的一個枚舉,在第一次被真正用到的時候,會被虛擬機加載並初始化,而這個初始化過程是線程安全的。而咱們知道,解決單例的併發問題,主要解決的就是初始化過程當中的線程安全問題。

因此,因爲枚舉的以上特性,枚舉實現的單例是天生線程安全的。

枚舉可解決反序列化會破壞單例的問題

前面咱們提到過,就是使用雙重校驗鎖實現的單例實際上是存在必定問題的,就是這種單例有可能被序列化鎖破壞,關於這種破壞及解決辦法,參看單例與序列化的那些事兒,這裏不作更加詳細的說明了。

那麼,對於序列化這件事情,爲何枚舉又有先天的優點了呢?答案能夠在Java Object Serialization Specification 中找到答案。其中專門對枚舉的序列化作了以下規定:

serialization

大概意思就是:在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是經過java.lang.EnumvalueOf方法來根據名字查找枚舉對象。同時,編譯器是不容許任何對這種序列化機制的定製的,所以禁用了writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve等方法。

普通的Java類的反序列化過程當中,會經過反射調用類的默認構造函數來初始化對象。因此,即便單例中構造函數是私有的,也會被反射給破壞掉。因爲反序列化後的對象是從新new出來的,因此這就破壞了單例。

可是,枚舉的反序列化並非經過反射實現的。因此,也就不會發生因爲反序列化致使的單例破壞問題。這部份內容在深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題中也有更加詳細的介紹,還展現了部分代碼,感興趣的朋友能夠前往閱讀。

總結

在全部的單例實現方式中,枚舉是一種在代碼寫法上最簡單的方式,之因此代碼十分簡潔,是由於Java給咱們提供了enum關鍵字,咱們即可以很方便的聲明一個枚舉類型,而不須要關心其初始化過程當中的線程安全問題,由於枚舉類在被虛擬機加載的時候會保證線程安全的被初始化。

除此以外,在序列化方面,Java中有明確規定,枚舉的序列化和反序列化是有特殊定製的。這就能夠避免反序列化過程當中因爲反射而致使的單例被破壞問題。

相關文章
相關標籤/搜索