關於單例模式的總結
常用的單例模式基本上只有靜態內部類和列舉兩種形式:
列舉
public enum SomeThing {
INSTANCE;
private Resource instance;
SomeThing() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
用法:
public static void main(String[] args) { SomeThing.INSTANCE.getInstance(); }
靜態內部類
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
用法:
public static void main(String[] args) {
Singleton.getInstance();
}
Joshua Bloch大神在《Effective Java》中明確表達過的觀點:
使用列舉實現單例的方法雖然還沒有廣泛採用,但是單元素的列舉型別已經成為實現Singleton的最佳方法。
通過列舉實現單例主要有以下幾個優點:
1)列舉實現單例的程式碼會精簡很多。
仔細閱讀單例模式的七種寫法可以發現大部分程式碼出於對執行緒安全問題的考慮,為了在保證執行緒安全和鎖粒度之間做權衡,程式碼難免會寫的複雜些。相比之下可以發現,列舉實現單例的程式碼會精簡很多。
2)列舉可解決執行緒安全問題
關於列舉的實現。這部分內容可以參考另外一篇博文《深度分析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方法,而這個方法使用同步程式碼塊保證了執行緒安全,ClassLoader
的loadClass
方法在載入類的時候使用了synchronized
關鍵字。)所以,建立一個enum型別是執行緒安全的。
也就是說,我們定義的一個列舉,在第一次被真正用到的時候,會被虛擬機器載入並初始化,而這個初始化過程是執行緒安全的。而我們知道,解決單例的併發問題,主要解決的就是初始化過程中的執行緒安全問題。
所以,由於列舉的以上特性,列舉實現的單例是天生執行緒安全的。
3)列舉可避免反序列化破壞單例
列舉可避免反序列化破壞單例前面我們提到過,使用“雙重校驗鎖”實現的單例其實是存在一定問題的,就是這種單例有可能被序列化鎖破壞,關於這種破壞及解決辦法,參看單例與序列化的那些事兒,這裡不做更加詳細的說明了。
那麼,對於序列化這件事情,為什麼列舉又有先天的優勢了呢?答案可以在Java Object Serialization Specification 中找到答案。其中專門對列舉的序列化做了如下規定:
大概意思就是:在序列化的時候Java僅僅是將列舉物件的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum
的valueOf
方法來根據名字查詢列舉物件。同時,編譯器是不允許任何對這種序列化機制的定製的,因此禁用了writeObject
、readObject
等方法。
除此之外,在序列化方面,Java中有明確規定,列舉的序列化和反序列化是有特殊定製的。這就可以避免反序列化過程中由於反射而導致的單例被破壞問題。
4)列舉可避免反射破壞單例
普通的Java類的反序列化過程中,會通過反射呼叫類的預設建構函式來初始化物件。所以,即使單例中建構函式是私有的,也會被反射給破壞掉。由於反序列化後的物件是重新new出來的,所以這就破壞了單例。由於列舉類不能在外部例項化物件,並且無償提供了序列化機制,絕對防止了多次例項化。
因為列舉的反序列化並不是通過反射實現的。所以,也就不會發生由於反序列化導致的單例破壞問題。這部分內容在《深度分析Java的列舉型別—-列舉的執行緒安全性及序列化問題》中也有更加詳細的介紹,還展示了部分程式碼,感興趣的朋友可以前往閱讀。
總結
在所有的單例實現方式中,列舉是一種在程式碼寫法上最簡單的方式,之所以程式碼十分簡潔,是因為Java給我們提供了enum
關鍵字,我們便可以很方便的宣告一個列舉型別,而不需要關心其初始化過程中的執行緒安全問題,因為列舉類在被虛擬機器載入的時候會保證執行緒安全的被初始化。
參考文章: