創建單例模式(Singleton)的幾種方式

單例模式可能是最常用到的設計模式了,但是想要正確的使用單例模式卻並不簡單。
我們先從最簡單最常用的方式開始:

懶漢式

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 版本

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章