設計之禪——單例模式詳解

一、前言

有時候我們只需要一個類只有一個對象,如,線程池、緩存、windows的任務管理器、註冊表等,因此就有了單例模式,確保了一個類只存在一個實例。單例模式的實現非常簡單,但是其中的細節也需要注意。下面我們就來看看他的各種實現。

二、實現

單例模式的實現方式有很多,根據是否立即創建對象分爲“懶漢”和“餓漢”兩大類別,即是否在類加載時立即創建對象,如果該對象頻繁被使用,可以使用“餓漢式”提高效率;反之則可以使用“懶漢式”來避免內存的浪費。而“懶漢式”的創建在多線程環境下則有許多方式來保證線程安全。

1. 懶漢式-線程不安全

public class Singleton {
	
    public static Singleton instance;
    
 	// 私有化構造方法,保證外部無法創建對象
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }

}

這種只能保證在單線下獲取到單例對象,並且在需要的時候纔會創建對象,故此稱爲“懶漢式”。但是因爲new Singleton()該操作並不是原子操作,當線程1執行到此時,可能還並未創建實例,那麼線程2在判斷instance==null時就會爲真,從而產生多個實例。

2. 懶漢式-線程安全

public class Singleton {

    private static Singleton instance;

	// 私有化構造方法,保證外部無法創建對象
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }

}

這種方式保證了線程安全,但是效率非常低,因此一般不推薦使用。

3. 餓漢式

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }

}

與懶漢式的區別是類加載的時候立即創建了對象實例,保證了對象始終只會有一個,但是如果該對象一直不被使用,就會浪費內存資源。

4. 靜態內部類

public class Singleton {

    private Singleton() {}

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }

}

使用靜態內部類來實現單例其實也是懶漢式的優化實現,利用類初始化時線程安全這一特點來創建單例對象,同時因爲是在靜態內部類中,有且僅當getInstance()方法被調用時纔會被初始化,所以也避免了內存的浪費。

5. 雙重校驗鎖

public class Singleton {

    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }

}

該方式是“餓漢式”的變種,保留了“餓漢式”的特點的同時保證了線程的安全。但是,需要注意的是volatile關鍵字是必須的,在網上很多文章上看到都沒帶這個關鍵字,如果不加可能會導致程序的崩潰。因此該方法只能在JDK1.5後使用。
volatile是保證線程之間的可見性。倘若沒有該關鍵字,假設線程1和線程2先後調用getInstance()方法,當線程1進入方法時判斷instance=null,因此去執行new Singleton()創建實例,上文提到該操作並非原子操作,會被編譯爲三條指令:

  1. 分配對象的內存空間;
  2. 初始化對象;
  3. 將對象指向內存地址;

而jvm會爲了執行效率而進行指令重排,重排後的指令順序爲:1->3->2,當指令執行完第3條指令,此時線程2進入方法進行第一次判斷時,就會得到一個並不完整的對象實例(因爲對象還未初始化,只是分配了內存空間),接着線程1執行完第2條指令,又會返回這個實例的完全態,但並不會立即刷新主內存,所以線程2並不能訪問到,程序就會出現錯誤導致崩潰。而volatile就是爲了處理這個問題,他能保證當某個線程改變對象實例後,立即刷新主內存,讓其他線程能夠同樣獲取到相同的實例對象,就不會出現不一致的問題了。

6. 枚舉

public enum Singleton {
    INSTANCE;
}

用枚舉的方式創建單例非常簡單明瞭,它本身能保證線程的安全,還能防止反序列化(readObject())導致對象不一致的問題,唯一的缺點則是同餓漢式一樣會立即創建對象實例(反編譯後可以看到),如果不考慮這點枚舉應是單例實現的最佳方式,也是《Effective Java》作者推薦的方式。

三、總結

單例模式是比較常用的模式之一,本文總結了6種實現方式,可以感受到看似簡單的代碼背後涉及到的細節非常多,因此也是非常考驗我們的基本功。在本文中並沒有考慮反射入侵的情況,有興趣的讀者們可自行研究。

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