天天看點

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

一、前言

有時候我們隻需要一個類隻有一個對象,如,線程池、緩存、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種實作方式,可以感受到看似簡單的代碼背後涉及到的細節非常多,是以也是非常考驗我們的基本功。在本文中并沒有考慮反射入侵的情況,有興趣的讀者們可自行研究。