“讀過書,……我便考你一考。茴香豆的茴字,怎樣寫的?”——魯迅《孔乙己》 0x00 大綱 0x01 前言 最近在重溫設計模式(in Java)的相關知識,然後在單例模式的實現上面進行了一些較深入的探究,有了一些以前不曾註意到的發現,遂將其整理成文,以作後用。 單例模式最初的定義出現於《設計模式》(艾 ...
“讀過書,……我便考你一考。茴香豆的茴字,怎樣寫的?”——魯迅《孔乙己》
0x00 大綱
目錄0x01 前言
最近在重溫設計模式(in Java)的相關知識,然後在單例模式的實現上面進行了一些較深入的探究,有了一些以前不曾註意到的發現,遂將其整理成文,以作後用。
單例模式最初的定義出現於《設計模式》(艾迪生維斯理, 1994):“保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。”
其應用場景可以說是十分廣泛,尤其是在涉及到資源管理方面的代碼,像應用配置(實例)、部分工具類或工廠類、JDK里的Runtime
等,都有出現單例模式的身影。
0x02 單例的正確性
探討單例模式有多少種實現方式的意義不是很大,因為單例模式的實現方式比茴字的寫法還多,但是正確的實現卻不多,我們不妨將重點放在如何保證單例的正確性上,從而尋求最佳實踐方案。
單例模式的關鍵在於如何保證“一個類僅有一個實例”。首先思考一下創建實例的方式有哪些?在Java語言裡面,有這幾種方式:new
關鍵字、clone
方法克隆、反序列化、反射。
new關鍵字
public class Main {
public static void main(String[] args) {
Singleton instance = new Singleton();
}
}
如果要保證一個類是單例,則必須阻止用戶通過new
關鍵字來隨意創建對象,最簡單粗暴的方法就是將構造方法私有化,然後提供一個靜態方法來進行實例的外部訪問:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
此時就不能在類的外部通過new
來創建對象了。
clone方法克隆
clone
方法是原型模式中創建複雜對象的方法,在Java中,clone
方法是Object
基類的方法,因此所有的類都會繼承該方法,但只有實現了Cloneable
介面的類才能正常調用clone
方法克隆對象實例,否則會拋出類型為CloneNotSupportedException
的異常,單例的類要防止用戶通過clone
方法克隆就不能實現Cloneable
介面。
反序列化
在Java裡面,實現了Serializable
介面的類可以通過ObjectOutputStream
將其實例序列化,然後再通過ObjectInputStream
進行反序列化,而在預設情況下,反序列之後得到的是一個新的實例,這就違背了單例的法則了。幸好JDK的開發人員也想到了這點,再Serializable
介面的文檔中有這樣一段描述:
Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature.
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
意思就是在反序列化時可以通過在類裡面定義readResolve
方法來指定反序列化時返回的對象,例如:
public class Singleton implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance = new Singleton();
private Singleton() {
if(instance != null) {
throw new RuntimeException("Not Allowed.");
}
}
public static Singleton getInstance() {
return instance;
}
private Object readResolve() throws java.io.ObjectStreamException {
return getInstance();
}
}
反射
聰明的你也許註意到了,上面的readResolve方法是private的。那麼它是怎麼被調用的呢?答案就是通過反射,想瞭解更詳細的調用過程可以去看看ObjectInputStream類源碼中的readOrdinaryObject方法。
通過反射可以無視private修飾符的限制調用類裡面的各種方法,也就是說用戶可以利用反射來調用我們的私有構造方法,像這樣:
public class Main {
public static void main(String[] args) throws Exception {
// 這句代碼無法執行,因為我們的構造方法是private的
// Singleton singleton = new Singleton();
// 通過反射來創建實例
java.lang.reflect.Constructor<Singleton> constructor;
constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton = constructor.newInstance();
// 兩個實例不一樣,單例完蛋
if(singleton != Singleton.getInstance()) {
System.out.println("哦嚯,完蛋");
}
}
}
解決方法是在構造方法裡面判斷類的實例是否已經被創建過,如果已經創建過的,拋出異常從而阻止反射調用。把單例類的代碼修改如下:
public class Singleton implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance = new Singleton();
private Singleton() {
if(instance != null) {
throw new RuntimeException("Not Allowed.");
}
}
public static Singleton getInstance() {
return instance;
}
/**
* 顯式指定反序列化時返回的單例對象
* @return
* @throws java.io.ObjectStreamException
*/
private Object readResolve() throws java.io.ObjectStreamException {
return getInstance();
}
}
再次通過反射進行對象創建時,就會拋出類型為RuntimeException
的異常,從而阻止新實例的創建。
0x03 最佳實踐方案
可以看到,我們為了實現單例模式,加入了一大堆膠水代碼,用於保證其正確性,這一點都不簡潔。那麼有沒有更簡單更有效的方式呢?有,而且已經有人幫我們驗證過了。
Joshua Bloch在《Effective Java》一書中寫道:
使用枚舉實現單例的方法雖然還沒有廣泛採用,但是單元素的枚舉類型已經成為實現Singleton的最佳方法。
我們直接上代碼看看:
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("do something.");
}
}
就是這麼簡單,再看看調用它的代碼:
public class Main {
public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
}
}
使用枚舉實現單例模式,不僅代碼簡潔,而且可以輕鬆阻止用戶通過new
關鍵字、clone
方法克隆、反序列化、反射等方式創建重覆實例,還保證線程安全,這一切由JVM替你操辦,不需要添加額外代碼。
0x04 驗證測試
枚舉實現單例模式能不能保證上面的提到的各種屬性呢?我們用代碼逐一驗證一下:
public class Main {
public static void main(String[] args) throws Exception {
// TEST-1: 驗證是否單一實例
EnumSingleton s1 = EnumSingleton.INSTANCE;
EnumSingleton s2 = EnumSingleton.INSTANCE;
if (s1.hashCode() != s2.hashCode()) {
System.out.println("哦嚯,完蛋");
} else {
System.out.println("TEST-1 PASSED.");
}
// TEST-2: 驗證反射創建
java.lang.reflect.Constructor<EnumSingleton> constructor;
// 註意這裡用的是枚舉的父構造器,因為我們沒有定義構造方法
constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
boolean passed = false;
try {
EnumSingleton s3 = constructor.newInstance("NEW_INSTANCE", 2);
} catch (Exception ex) {
// 報錯說明反射不能創建
passed = true;
}
if (!passed) {
System.out.println("哦嚯,完蛋");
} else {
System.out.println("TEST-2 PASSED.");
}
// TEST-3: 驗證反序列化
EnumSingleton s4 = EnumSingleton.INSTANCE;
EnumSingleton s5;
try (java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(new java.io.FileOutputStream("EnumObject"))) {
oos.writeObject(s4);
}
try (java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new java.io.FileInputStream("EnumObject"))) {
s5 = (EnumSingleton) ois.readObject();
}
if (s4.hashCode() != s5.hashCode()) {
System.out.println("哦嚯,完蛋");
} else {
System.out.println("TEST-3 PASSED.");
}
// TEST-4: 多線程測試
java.util.concurrent.CountDownLatch begin = new java.util.concurrent.CountDownLatch(10);
java.util.concurrent.CountDownLatch end = new java.util.concurrent.CountDownLatch(10);
java.util.Set<EnumSingleton> set = new java.util.HashSet<>(1024);
java.util.stream.IntStream.range(0, 20).forEach(
i -> {
new Thread(() -> {
try {
begin.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
set.add(EnumSingleton.INSTANCE);
System.out.println(System.currentTimeMillis() + ":" + Thread.currentThread().getName() + "->" + EnumSingleton.INSTANCE.hashCode());
end.countDown();
}).start();
begin.countDown();
}
);
end.await();
if(set.size() != 1) {
System.out.println("哦嚯,完蛋");
} else {
System.out.println("TEST-4 PASSED.");
}
}
}
測試結果:
TEST-1 PASSED.
TEST-2 PASSED.
TEST-3 PASSED.
...
TEST-4 PASSED.
0x05 真的是最佳實踐嗎
在 Java Language Specification 枚舉類型這一章節中,具體闡述了若幹點對於枚舉類型的強制和隱性約束:
An enum declaration specifies a new enum type, a special kind of class type.
It is a compile-time error if an enum declaration has the modifier abstract or final.
An enum declaration is implicitly final unless it contains at least one enum constant that has a class body (§8.9.1).
A nested enum type is implicitly static. It is permitted for the declaration of a nested enum type to redundantly specify the static modifier.
This implies that it is impossible to declare an enum type in the body of an inner class (§8.1.3), because an inner class cannot have static members except for constant variables.
It is a compile-time error if the same keyword appears more than once as a modifier for an enum declaration.
The direct superclass of an enum type E is Enum
(§8.1.4). An enum type has no instances other than those defined by its enum constants. It is a compile-time error to attempt to explicitly instantiate an enum type (§15.9.1).
其中最為突出和有影響是以下兩點:
不能顯式繼承
和常規類一樣,枚舉可以實現介面,並提供公共實現或每個枚舉值的單獨實現,但不能繼承,因為所有的枚舉預設隱式繼承了Enum<E>
類型,不能繼承也就意味著喪失了一部分的抽象能力(不能定義abstract
方法),雖然可以通過組合的方式變通實現,但這無疑犧牲了擴展性和靈活性。
無法延遲載入
因為枚舉實例化的特殊性,所有的構造器屬性都必須在枚舉創建時指定,無法在運行時通過代碼動態傳遞和構造。
0x06 小結
非枚舉的單例實現除開少數極端場景,在大多數時候下也都夠用了,且保留了OOP的靈活特性,方便日後業務擴展,基於枚舉的單例實現有序列化和線程安全的保證,而且只要幾行代碼就能實現,不失為一種有效的方案,但並不無敵。具體的實現方案還是要根據業務背景和實際情況來進行選擇,畢竟,軟體工程沒有銀彈。