1 概述
單例模式應該是最簡單,同時又是最複雜的一種創建型模式。因爲大家都知道這個模式:無非就是保證某個對象在系統中只存在一個實例。然而想要真正實現一個完美的單例模式,卻不簡單。
2 單例模式
一般單例模式的實現,都需要包含兩個步驟:
- 將類的構造函數私有化。
- 提供一個
public
的方法,以供外界獲取唯一的實例。
下面將一一介紹單例模式的各種實現方式。
3 案例
3.1 註冊表式
提供一個註冊表類,來維護所有單例的實例
public class Test {
public static void main(String[] args) {
SampleClass singleton1 = Registry.getInstance(SampleClass.class);
SampleClass singleton2 = Registry.getInstance(SampleClass.class);
System.out.println("Registry singleton instance1: " + singleton1.hashCode());
System.out.println("Registry singleton instance2: " + singleton2.hashCode());
System.out.println("We can broke singleton by new a instance through class's construct method");
SampleClass singleton3 = new SampleClass();
System.out.println("Registry singleton instance3: " + singleton3.hashCode());
}
}
public class Registry {
private static Map<Class, Object> registry = new ConcurrentHashMap<>();
private Registry() {};
public static synchronized <T> T getInstance(Class<T> type) {
Object obj = registry.get(type);
if (obj == null) {
try {
obj = type.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
registry.put(type, obj);
}
return (T) obj;
}
}
public class SampleClass {
}
輸出:
Registry singleton instance1: 21685669
Registry singleton instance2: 21685669
We can broke singleton by new a instance through class's construct method
Registry singleton instance3: 2133927002
用註冊表實現的單例其實是僞單例,因爲它只能保證從註冊表中獲取的對象是全局唯一的。如果我們不從註冊表獲取,而是直接new
一個實例,這顯然破壞了單例模式。我們熟悉的Spring
框架,就是用這種模式實現的單例,其中的Registry
就是BeanFactory
。
要從根本上實現實例的全局唯一,我們必須在單例類本身下功夫。
3.1 餓漢式----靜態屬性
將實例作爲類的一個靜態變量,來實現唯一性:
public class StaticFieldTest {
public static void main(String[] args) {
StaticFieldSingleton fieldSingleton1 = StaticFieldSingleton.getInstance();
StaticFieldSingleton fieldSingleton2 = StaticFieldSingleton.getInstance();
System.out.println("StaticFieldSingleton instance1: " + fieldSingleton1.hashCode());
System.out.println("StaticFieldSingleton instance1: " + fieldSingleton2.hashCode());
}
}
public class StaticFieldSingleton {
private static StaticFieldSingleton singletonInstance = new StaticFieldSingleton();
// 將構造方法私有化
private StaticFieldSingleton(){};
// 提供唯一的接口,供外部獲取唯一的變量
public static StaticFieldSingleton getInstance() {
return singletonInstance;
}
}
輸出:
StaticFieldSingleton in multi-thread instance: 837048303
StaticFieldSingleton in multi-thread instance: 837048303
當類StaticFieldSingleton
被加載進JVM
的時候,類的實例會作爲類的靜態屬性,隨着類一起初始化。這種實現方式其實是依靠類加載器來保證實例的唯一性。優點是,不需要考慮多線程加鎖,實現起來比較簡單。缺點是,無論後續是否會用到,實例都會在class
被加載的時候被創建好。這對於內存資源比較寶貴的場景,或者目標是某些如File System
的大對象的時候,會導致資源的浪費。同時,這種方式也無法提供對異常的處理,在某些情況下,會導致程序出錯。
3.2 餓漢式----靜態塊
將類實例的初始化放在類的靜態塊中:
public class StaticBlockTest {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
StaticBlockSingleton staticBlockSingleton = StaticBlockSingleton.getInstance();
System.out.println("StaticBlockSingleton in multi-thread instance: " + staticBlockSingleton.hashCode());
}).start();
}
}
}
public class StaticBlockSingleton {
private static StaticBlockSingleton singletonInstance;
private StaticBlockSingleton(){};
// 靜態塊會在類被加載進內存的時候被執行
static {
try {
singletonInstance = new StaticBlockSingleton();
} catch (Exception e) {
e.printStackTrace();
}
}
public static StaticBlockSingleton getInstance() {
return singletonInstance;
}
}
輸出:
StaticBlockSingleton in multi-thread instance: 2132107705
StaticBlockSingleton in multi-thread instance: 2132107705
靜態塊中初始化與靜態變量上初始化本質上是一樣的,都是通過類加載器來保證實例只會被初始化一次。區別是,靜態塊初始化可以做異常的捕獲與處理,同時還允許我們在靜態塊中做一些額外的事情,比靜態變量的方式更自由。
但兩種餓漢式都不可避免地會造成額外內存的佔用,於是出現了按需加載的懶漢式創建方式。
3.3 懶漢式----基礎版
將類實例的初始化放在方法中。只有當方法第一次被訪問的時候,去初始化實例:
public class SynchronizedTest {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
SynchronizedSingleton synchronizedSingleton = SynchronizedSingleton.getInstance();
System.out.println("SynchronizedSingleton in multi-thread instance: " + synchronizedSingleton.hashCode());
}).start();
}
}
}
public class SynchronizedSingleton {
private static SynchronizedSingleton singletonInstance;
private SynchronizedSingleton(){};
// 加了同步鎖,保證new SynchronizedSingleton()只會被第一個線程訪問
public static synchronized SynchronizedSingleton getInstance() {
if (singletonInstance == null) {
singletonInstance = new SynchronizedSingleton();
}
return singletonInstance;
}
}
輸出:
SynchronizedSingleton in multi-thread instance: 554449003
SynchronizedSingleton in multi-thread instance: 554449003
懶漢式解決了餓漢式存在的最大問題:可能導致的內存浪費。只有當getInstance()
方法第一次被訪問的時候,實例纔會去真正創建。而方法上加了synchronized
,保證了後續對方法的訪問,都只會返回之前創建好的實例,保證了唯一性。
這種方式的不足是,每次對getInstance()
方法的訪問,都需要獲取鎖,衆所周知,鎖的獲取與釋放是一筆昂貴的開銷。而事實上只有當第一次實例創建的時候需要加鎖。於是有了改進的方式:雙檢鎖。
3.4 懶漢式----雙檢鎖
雙檢鎖(Double Check Lock)是一個很多人都熟悉的概念,是上述模式的增強版。實現如下:
public class DoubleCheckLockTest {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
DoubleCheckLockSingleton doubleCheckLockSingleton = DoubleCheckLockSingleton.getInstance();
System.out.println("DoubleCheckLockSingleton in multi-thread instance: " + doubleCheckLockSingleton.hashCode());
}).start();
}
}
}
public class DoubleCheckLockSingleton {
// 變量必須聲明爲volatile,否則可能會得到一個“半初始化”的實例
private static volatile DoubleCheckLockSingleton singletonInstance;
private DoubleCheckLockSingleton(){};
// 若實例已經被創建,則不需要再進入同步塊
// 若實例還沒創建,則在同步塊中檢查並創建實例
public static DoubleCheckLockSingleton getInstance() {
DoubleCheckLockSingleton instance = singletonInstance;
if (instance == null) {
synchronized (DoubleCheckLockSingleton.class) {
instance = singletonInstance;
if (instance == null) {
instance = singletonInstance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
}
輸出:
DoubleCheckLockSingleton in multi-thread instance: 837048303
DoubleCheckLockSingleton in multi-thread instance: 837048303
單例模式中,水最深的應該就是是雙檢鎖了。在上述實現中,有幾個要點:
- 爲什麼需要兩次
if
檢查:第一次if
檢查在synchronized
塊之外,當實例已經被創建好之後,可以立即返回。第二次if
檢查,是因爲在高併發的情況下,可能會有好多線程走到第一個if
塊中,去爭搶synchronized
鎖,我們必須保證只有第一個搶到鎖的線程能創建實例,所以後面的線程必須再進行一次if
判斷,發現實例已經被第一個搶到鎖的線程初始化好了,直接返回該實例。這也是雙檢名字的由來。 - 爲什麼成員變量
singletonInstance
要聲明爲volatile
:因爲new DoubleCheckLockSingleton()
其實並不是一個原子操作,主要可以分爲給實例分配堆內存,執行類的構造函數,將實例引用賦給調用者三步。而由於重排序的存在,在某一些機器上,第三步會先於第二步發生,於是可能出現,線程A走到了new DoubleCheckLockSingleton()
,但並未執行完構造函數時,線程B發現instance != null
了,於是對instance
的屬性進行訪問,結果看到的屬性都是默認值。而JMM
在Java1.5
之後進行了增強,volatile
關鍵字可以禁止編譯器的重排序,並會在volatile
關鍵字修飾的變量前後適當位置添加內存屏障,保證程序不會讀到半初始化的實例。關於JMM
的增強,可以擴展閱讀Doug Lea大神的文章。 - 爲什麼要加局部變量
instance
:加這個局部變量,主要是爲了提高程序的性能。因爲成員變量singletonInstance
是聲明爲volatile
的,而所有對volatile
變量的操作(讀寫)都必須與主內存交互,開銷相對較大。加局部變量可以減少與volatile
變量的交互。這也是java.util.concurrent
包中很多工具類的常見做法。
到這裏,似乎雙檢鎖的方案已經很完美了,確實,這也是被很多人所採用的單例模式實現方案。但其實懶漢式還有一種更爲通用的實現方式。
3.5 懶漢式----靜態內部類
引入一個靜態內部類,來實現對靜態變量的延遲加載:
public class InnerClassWrappedTest {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
InnerClassWrappedSingleton innerClassWrappedSingleton = InnerClassWrappedSingleton.getInstance();
System.out.println("InnerClassWrappedSingleton in multi-thread instance: " + innerClassWrappedSingleton.hashCode());
}).start();
}
}
}
public class InnerClassWrappedSingleton {
private InnerClassWrappedSingleton(){};
// 內部類持有單例,僅當getInstance()方法被調用的時候,SingletonHolder類纔會被加載
// final關鍵字保證了不會得到“半初始化”的實例
private static class SingletonHolder {
private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();
}
public static InnerClassWrappedSingleton getInstance() {
return SingletonHolder.instance;
}
}
輸出:
InnerClassWrappedSingleton in multi-thread instance: 2132107705
InnerClassWrappedSingleton in multi-thread instance: 2132107705
上述實現其實可以看作是餓漢式----靜態塊的升級版,只不過把實例的初始化,放到了靜態內部類中。而該靜態內部類只有在getInstance()
被調用的時候,纔會被加載,從而對單例進行初始化。同樣,由類加載器保證了,只有一個實例會被創建。同時,final
關鍵字在Java1.5
之後也進行了增強,可以保證得到的一定是一個完整的單例。
這種方式是本人覺得比較好的方式,因爲實現簡單線程安全,而且適用性很強。
3.6 破壞單例----序列化
其實所有上述的實現方式,都不可能完全保證類的唯一,因爲儘管我們把類的構造器設爲了private
,但仍然有辦法用其他方式創建新的實例。比如不巧,單例的類正好實現了Serializable
接口,那麼黑客們可以通過序列化的方式,得到一個新的“單例”:
public class SingletonDestroyerSerialization {
public static void main(String[] args) throws IOException, ClassNotFoundException {
InnerClassWrappedSingleton instance1 = InnerClassWrappedSingleton.getInstance();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
// 將單例序列化
oos.writeObject(instance1);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
// 反序列化,創建一個新的“單例”
InnerClassWrappedSingleton instance2 = (InnerClassWrappedSingleton) ois.readObject();
System.out.println("singleton instance1: " + instance1.hashCode());
System.out.println("singleton instance2: " + instance2.hashCode());
}
}
public static class InnerClassWrappedSingleton implements Serializable {
private InnerClassWrappedSingleton(){};
private static class SingletonHolder {
private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();
}
public static InnerClassWrappedSingleton getInstance() {
return SingletonHolder.instance;
}
}
輸出:
singleton instance1: 1173230247
singleton instance2: 764977973
顯然,輸出了不同的hashcode
,JVM
中存在了兩個“單例”對象。
爲了防止以上情況出現,我們可以在單例類中,添加一個readResolve()
方法,並返回單例實例。這樣,在反序列化之後,我們得到的依然是原先的實例:
public static class InnerClassWrappedSingleton implements Serializable {
private InnerClassWrappedSingleton(){};
private static class SingletonHolder {
private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();
}
public static InnerClassWrappedSingleton getInstance() {
return SingletonHolder.instance;
}
// 添加此方法,防止序列化與反序列化創建新的實例
private Object readResolve() {
return SingletonHolder.instance;
}
}
3.7 破壞單例----反射
如果說序列化與反序列化我們還有應對的辦法,那麼對於反射攻擊,上述所有的實現方案,都無可奈何:
public class SingletonDestroyerRefelct {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
InnerClassWrappedSingleton instance1 = InnerClassWrappedSingleton.getInstance();
System.out.println("singleton instance1: " + instance1.hashCode());
Constructor[] constructors = InnerClassWrappedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
// 利用反射,創建一個新的“單例”變量
constructor.setAccessible(true);
InnerClassWrappedSingleton instance2 = (InnerClassWrappedSingleton) constructor.newInstance();
System.out.println("singleton instance2: " + instance2.hashCode());
break;
}
}
}
public static class InnerClassWrappedSingleton {
private InnerClassWrappedSingleton(){};
private static class SingletonHolder {
private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();
}
public static InnerClassWrappedSingleton getInstance() {
return SingletonHolder.instance;
}
}
輸出:
singleton instance1: 1735600054
singleton instance2: 21685669
反射其實類似Java
中的一個後門,非常強大,它能破壞單例模式也是情理之中。類似JSON
序列化與反序列化,也能創建多個不同的“單例”,利用的也是反射機制。
3.8 究極單例----Enum
有沒有辦法防止反射調用破壞單例呢?答案是肯定的,即用enum
創建單例:
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
}
}
首先,JVM
對enum
型變量的序列化與反序列化做了特殊處理,保證反序列化之後得到的依然是內存中的那個enum
。
第二,Java從語言層面保證,無法通過反射創建enum
類型變量。
所以,如果說要選一種最安全的單例模式實現方案,那非Enum模式莫屬。這也是「Effective Java」的作者Joshua Bloch所推薦的方式。
4 總結
本文介紹了形形色色很多的單例模式,其實也並不是越到後面的實現越好,而是要看每個版本的特性,選擇最適合自己項目的那個版本。