為什麽我墻裂建議大家使用枚舉來實現單例
哪種寫單例的方式最好
在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類的加載和初始化過程都是線程安全的(因為虛擬機在加載枚舉的類的時候,會使用ClassLoader的loadClass方法,而這個方法使用同步代碼塊保證了線程安全)。所以,創建一個enum類型是線程安全的。
也就是說,我們定義的一個枚舉,在第一次被真正用到的時候,會被虛擬機加載並初始化,而這個初始化過程是線程安全的。而我們知道,解決單例的並發問題,主要解決的就是初始化過程中的線程安全問題。
所以,由於枚舉的以上特性,枚舉實現的單例是天生線程安全的。
枚舉可避免反序列化破壞單例
前面我們提到過,使用“雙重校驗鎖”實現的單例其實是存在一定問題的,就是這種單例有可能被序列化鎖破壞,關於這種破壞及解決辦法,參看單例與序列化的那些事兒,這裏不做更加詳細的說明了。
那麽,對於序列化這件事情,為什麽枚舉又有先天的優勢了呢?答案可以在Java Object Serialization Specification 中找到答案。其中專門對枚舉的序列化做了如下規定:
大概意思就是:在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定制的,因此禁用了writeObject、readObject等方法。
普通的Java類的反序列化過程中,會通過反射調用類的默認構造函數來初始化對象。所以,即使單例中構造函數是私有的,也會被反射給破壞掉。由於反序列化後的對象是重新new出來的,所以這就破壞了單例。
但是,枚舉的反序列化並不是通過反射實現的。所以,也就不會發生由於反序列化導致的單例破壞問題。這部分內容在《深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題》中也有更加詳細的介紹,還展示了部分代碼,感興趣的朋友可以前往閱讀。
總結
在所有的單例實現方式中,枚舉是一種在代碼寫法上最簡單的方式,之所以代碼十分簡潔,是因為Java給我們提供了enum關鍵字,我們便可以很方便的聲明一個枚舉類型,而不需要關心其初始化過程中的線程安全問題,因為枚舉類在被虛擬機加載的時候會保證線程安全的被初始化。
除此之外,在序列化方面,Java中有明確規定,枚舉的序列化和反序列化是有特殊定制的。這就可以避免反序列化過程中由於反射而導致的單例被破壞問題。
為什麽我墻裂建議大家使用枚舉來實現單例