單例模式
按照慣有的分類方式,設計模式總共分爲3大類:1、創建型 ,2、結構型, 3、行爲型。
單例模式便是創建型設計模式的一種,它確保某一個類在系統中只有一個實例,並自行實例化,同時向外部提供獲取這個唯一實例的接口。從這段描述中,我們不難可以得到單例模式的三大特性:
- 單例類只有一個實例。
- 單例類必須自己實例化自己。
- 單例類需要向外提供實例。
雖然單例設計模式算是“入門級“的設計模式,但依然需要我們仔細去理解它的特性是如何通過代碼實現,這些代碼背後的原理又是什麼。
下面,本文通過分析5中經典的單例模式寫法,逐步分析寫法的成因與背後原理。
餓漢式
public class EagerSingleton {
// 靜態變量,類在創建之初就會執行實例化動作。
private static EagerSingleton instance = new EagerSingleton();
// 私有化構造函數,使外界無法創建實例
private EagerSingleton(){}
// 爲外界提供獲取實例接口
public static EagerSingleton getInstance(){
return instance;
}
}
上面是餓漢式單例模式的標準代碼,所謂的“餓漢式”只是形象的比喻:EagerSingleton
類的實例因爲變量instance
申明爲static
的關係,在類加載過程中便會執行。由此帶來的好處是Java的類加載機制本身爲我們保證了實例化過程的線程安全性,缺點是這種空間換時間的方式,即使類實例本身還未用到,實例也會被創建。
餓漢式的缺點有2:
- 空間使用率不高
- 類加載時實例化,意味着該類無法在程序運行過程中通過運行參數實例化,代碼失去靈活性。
餓漢式在當前硬件設備條件下,缺點其實關係不大,對於空間不是特別嚴苛的應用來說,且用不到初始化參數的類型來說,我非常建議使用這種方式。
懶漢式
“懶漢式”是針對餓漢式單例模式缺點而生的懶加載模式,所謂懶加載的意思是,只有當需要使用實例的時候纔去實例化。來看看示例代碼:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton(){}
// 爲外界提供獲取實例接口
public static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton(); // 懶加載
}
return instance;
}
}
餓漢式和懶漢式的區別在於,餓漢式在類加載時便被實例化,而懶漢式是在getInstance()
函數調用時,相信你也能看出來,當instance == null
時,去實例化,否則直接返回實例。
但這裏有個問題,單例模式的核心是系統中只存在一個單例類的實例,這其實隱含了實例只創建一次的意思。但上述LazySingleton
類只能保證在單線程中只創建一次,在多線程中卻不能保證。
如果有兩個線程,Thread1
、Thread2
,兩個線程先後調用getInstance()
函數。如果Thread1
的調用,執行到if(instance == null)
的語句塊中被中斷,此時instance
的值還未改變,Thread2
也執行到了這裏,可以預見,兩個線程都將分別創建一個LazySingleton
實例,最終instance
的值是那個線程創建的實例,將是不確定的。
這個缺點的原因,涉及到併發編程的原子性。實例中,創建實例的代碼邏輯失去了原子性從而導致可能存在多個實例創建的情況。
原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
既然這樣,我們給實例代碼加上原子性就好了。
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton(){}
// 爲外界提供獲取實例接口
public static synchronized LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton(); // 懶加載
}
return instance;
}
}
synchronized
是Java中實現代碼塊原子性的關鍵字之一,getInstance()
函數加上了原子性後,確實解決了問題。但這又引入了新的問題。
在getInstance()
函數申明上加synchronized
,意味着每次函數調用都會進行同步檢查,這是低效的。實際上,我們只需要保證創建實例代碼的原子性即可。即:
if(instance == null){
instance = new LazySingleton(); // 懶加載
}
也就是說,這種實現方案的同步範圍擴大了,這個問題由雙重檢查鎖來解決。
雙重檢查鎖
在前面,我們在getInstance()
加了synchronized
,擴大了同步範圍,現在我們來減小一下同步範圍:
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
//先檢查實例是否存在,如果不存在才進入下面的同步塊
if(instance == null){
//同步塊,線程安全的創建實例
synchronized (Singleton.class) {
//再次檢查實例是否存在,如果不存在才真正的創建實例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
所謂的雙重檢查,是在同步前後的兩次if(instance == null)
判斷,是否已經存在實例,鎖自然指的就是synchronized
關鍵字。
對着代碼,我們再來考慮兩個線程同時通過了第一道if(instance == null)
檢查,但因爲同步鎖是互斥的,只能第一個線程釋放後,第二個線程才能持有。這保證了同步代碼塊的原子性,在同步代碼塊中的如果instance還爲創建,此時纔會創建。
此外,還需要注意的是volatile
關鍵字的使用。
在instance = new Singleton();
這行代碼執行時,虛擬機大概可以分爲三個指令步驟:
- 在內存中給Singleton實例分配空間
- 調用Singleton構造函數,初始化成員
- 爲Singleton實例指向第一步分配的內存空間(此時instance不爲空)
代碼在編譯時,存在指令優化的現象。指令優化只保證單線程條件下執行結果一致,而不保證執行的順序。所以前面三個指令的執行順序是不確定的,可能是1-2-3,也可以是1-3-2。如果順序是1-3-2,當第三步執行完後,instance
已經不爲空了,但成員並未初始化,第二個線程使用該instance
自然會報錯。怎麼解決呢?
volatile
可以解決這個問題,該關鍵字可以確保相關變量涉及的代碼指令不被優化順序。
來看看雙重檢查鎖的代碼,即實現了線程安全,也實現了懶加載。已經很完美了,唯一的缺點是,有點兒太複雜。
靜態內部類
看到這裏,應該也明白了,最好的單例現實,需要滿足兩個條件:1. 線程安全。2. 懶加載。在前面,這兩點都被我們手動一一實現。可不可以不用自己手動實現呢?當然,來看下面的代碼。
public class Singleton {
private Singleton(){}
// 只有當類被調用時,纔會加載
private static class SingletonHolder{
// 靜態初始化器,由JVM來保證線程安全
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
- 線程安全:由靜態內部類中的靜態成員初始化時創建實例,通過JVM類加載機制來保證線程的安全性。
- 懶加載:使用靜態內部類的方式,讓類
SingletonHolder
只有在使用的時候纔會被加載,實例纔會創建,藉機實現了懶加載。
那麼還有沒有更簡單的呢?
枚舉
public enum Singleton {
uniqueInstance;
public void singletonOperation(){
// 單例類的其它操作
}
}
雖然《高效Java 第二版》中說,單元素的枚舉類型是實現單例的最佳方法。
雖然說使用枚舉的方式確實簡潔方便,又不怕出錯,但我覺得還是不能滿足這一點:無法在程序運行過程中通過運行參數實例化,代碼失去靈活性。