單例模式可能是最常用到的設計模式了,但是想要正確的使用單例模式卻並不簡單。
我們先從最簡單最常用的方式開始:
懶漢式
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
要點:
-
私有的靜態內部引用實例
-
私有構造函數
-
共有靜態的getInstance()方法,當靜態內部引用爲空時才實例化
缺點:
-
多線程環境下不安全
餓漢式
考慮到多線程的條件,還有另外一種常用的簡單實現方式:
public class Singleton{
private final static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
要點:
-
private, final 和 static 的實例變量
-
私有化構造函數
-
共有靜態的getInstance()方法
-
static 的實例變量在類加載到內存的時候就會初始化,創建實例是線程安全的
缺點:
-
實例在類初始化一開始就被創建了,哪怕後來根本沒有使用它
-
如果實例的創建時依賴於外部的參數/文件的話,這種方式就不適用了
雙重檢驗鎖
爲了避免上面餓漢式的缺點,我們來考慮改進懶漢式單例模式來支持多線程的情況。最直接的想法就是對
getInstance()
加鎖,但是這樣一來同一時間只能有一個線程調用單例實例,效率低下。通過分析,我們可以發現其實不用對整個 getInstance()方法加鎖,只需要在實例爲空需要創建時加鎖。
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
要點:
-
兩次檢查
instance == null
,一次是在同步塊外,一次是在同步塊內
使用兩次判斷的原因:有可能多個線程同時進入第一個
if
判斷,如果在同步塊中不再次判斷的話,有可能生成多個實例
缺點:
-
由於JVM指令重排序的優化,在
instance = new Singleton();
仍有可能生成多個實例
在JVM指令優化時,
instance = new Singleton();
並不是一個原子操作,而是3個步驟:
1. 爲instance分配內存
2. 調用 Singleton構造函數初始化成員變量
3. 將instance對象指向分配的內存空間 (instance非null)
在JVM編譯優化時,上面3個步驟並不是順序執行的,有可能重新排列執行的順序,有可能是 1-2-3, 或者 1-3-2。如果是 1-3-2的執行順序的話,有可能出現這種情況:線程1執行完了1-3步驟後退出了同步塊,這個時候instance已經是非null了,但還沒被初始化,這個時候線程2進入同步塊,判斷instance爲非null,所有直接返回沒有初始化的對象,在後面的使用中自然會報錯。
靜態內部類
public class Singleton{
private static class SingletonHolder{
private static final Singleton INSTANE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
優點:
- 使用JVM本身機制保證了線程安全問題;
- SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;
- 讀取實例的時候不會進行同步,沒有性能缺陷;
- 不依賴 JDK 版本