Java —— Singleton 單例模式

Java —— Singleton 單例模式

創造型模式

  • 想確保任何情況下都絕對只有1個實例
  • 想在程序上表現出“只存在一個實例”

簡介

單例模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

注意

  • 1、單例類只能有一個實例。
  • 2、單例類必須自己創建自己的唯一實例。
  • 3、單例類必須給所有其他對象提供這一實例。

用處

當想控制實例數目,節省系統資源的時候。可以解決一個全局使用的類頻繁地創建與銷燬的問題。

簡單例子

基礎示例

  • Singleton類
public class SingleObject {

    /**
     * 創建 SingleObject 的一個對象
     */
    private static SingleObject instance = new SingleObject();

    /**
     * 讓構造函數爲 private,這樣該類就不會被實例化
     */
    private SingleObject(){}

    /**
     * 獲取唯一可用的對象
     * @return
     */
    public static SingleObject getInstance(){
        return instance;
    }

    public void showMessage(){
        System.out.println("Hello World!");
    }

}
  • 測試類
public class SingletonPatternDemo {

    public static void main(String[] args) {
        //不合法的構造函數
        //編譯時錯誤:構造函數 SingleObject() 是不可見的
        //SingleObject object = new SingleObject();

        //獲取唯一可用的對象
        SingleObject object = SingleObject.getInstance();

        //顯示消息
        object.showMessage();
    }

}
  • 運行結果
Hello World!

懶漢式(線程不安全)

Lazy 初始化:是
多線程安全:否

最大的問題就是不支持多線程。因爲沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式。
這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。

public class Singleton {

    private static Singleton instance;

    private Singleton (){}

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

}

懶漢式(線程安全)

Lazy 初始化:是
多線程安全:是
這種方式具備很好的 lazy loading(懶加載),能夠在多線程中很好的工作,但是,效率很低,99% 情況下不需要同步。

  • 優點
    第一次調用才初始化,避免內存浪費。
  • 缺點
    必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。
public class Singleton1 {

    private static Singleton1 instance;

    private Singleton1 (){}

    /**
     * 與懶漢式加載(線程不安全但區別在於使用了 synchronized 聲明)
     * @return
     */
    public static synchronized Singleton1 getInstance() {
        if (instance == null) {
            instance = new Singleton1();
        }
        return instance;
    }

}

餓漢式

Lazy 初始化:是
多線程安全:是
這種方式比較常用,但容易產生垃圾對象

  • 優點
    沒有加鎖,執行效率會提高。
  • 缺點
    類加載時就初始化,浪費內存。

基於 classloader 機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化 instance 顯然沒有達到 lazy loading 的效果。

public class Singleton2 {
	// instance 在類裝載時就實例化
    private static Singleton2 instance = new Singleton2();
    
    private Singleton2 (){}
    
    public static Singleton2 getInstance() {
        return instance;
    }

}

雙檢索 / 雙重校驗鎖(DCL,即 double-checked locking)

JDK 版本:1.5起
Lazy 初始化:是
多線程安全:是
這種方式採用雙鎖機制,安全且在多線程情況下能保持高性能。
getInstance() 的性能對應用程序很關鍵。

public class Singleton3 {

    /**
     * volatile 修飾的成員變量在每次被線程訪問時,都強制從共享內存中重新讀取該成員變量的值
     */
    private volatile static Singleton3 Singleton3;

    private Singleton3 (){}

    public static Singleton3 getSingleton3() {
        if (Singleton3 == null) {
            /**
             * synchronized 關鍵字聲明的方法同一時間只能被一個線程訪問。
             */
            synchronized (Singleton3.class) {
                if (Singleton3 == null) {
                    Singleton3 = new Singleton3();
                }
            }
        }
        return Singleton3;
    }

}

登記式/靜態內部類

Lazy 初始化:是
多線程安全:是
達到雙檢鎖方式一樣的功效,但實現更簡單。
對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用於靜態域的情況,雙檢鎖方式可在實例域需要延遲初始化時使用。
這種方式同樣利用了 classloader 機制來保證初始化 instance 時只有一個線程,它跟第 餓漢式 種方式不同的是: 餓漢式 方式只要 Singleton 類被裝載了,那麼 instance 就會被實例化(沒有達到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不一定被初始化。因爲 SingletonHolder 類沒有被主動使用,只有通過顯式調用 getInstance 方法時,纔會顯式裝載 SingletonHolder 類,從而實例化 instance。想象一下,如果實例化 instance 很消耗資源,所以想讓它延遲加載,另外一方面,又不希望在 Singleton 類加載時就實例化,因爲不能確保 Singleton 類還可能在其他的地方被主動使用從而被加載,那麼這個時候實例化 instance 顯然是不合適的。這個時候,這種方式相比 餓漢式 方式就顯得很合理。

public class Singleton4 {
    
    private static class Singleton4Holder {
        private static final Singleton4 INSTANCE = new Singleton4();
    }
    
    private Singleton4 (){}

    /**
     * 當Singleton使用時,不會初始化 INSTANCE,只有在實際調用getInstance()時,纔會初始化對象。
     * @return
     */
    public static final Singleton4 getInstance() {
        return Singleton4Holder.INSTANCE;
    }
}

枚舉

Lazy 初始化:否
多線程安全:是
實現單例模式的最佳方法[runoob推薦]。它更簡潔,自動支持序列化機制,絕對防止多次實例化。
這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化。不過,由於 JDK1.5 之後才加入 enum 特性,用這種方式寫不免讓人感覺生疏,在實際工作中,也很少用。
不能通過 reflection attack 來調用私有構造方法。

public enum Singleton5 {
    
    INSTANCE;
    
    public void whateverMethod() {
        
    }
}

推薦:一般情況下,不建議使用前兩種懶漢方式,建議使用餓漢方式。只有在要明確實現 lazy loading 效果時,纔會使用登記方式。如果涉及到反序列化創建對象時,可以嘗試使用枚舉方式。如果有其他特殊的需求,可以考慮使用雙檢鎖方式。

涉及角色

  • Singleton
    單例模式中,只有Singleton一個角色。
    Singleton角色中有一個返回唯一實例的static方法。該方法總是會返回一個實例。

要點

構造函數

構造函數一定要是私有的。

相關的設計模式

  • AbstractFactory 抽象工廠模式
  • Builder 建造者模式
  • Facade 外觀模式
  • Prototype 原型模式

應用實例

  • 一個班級只有一個班主任。
  • 多進程多線程環境下,出現多個進程或線程操作同一個文件的現象,所有的文件處理必須通過唯一的實例來進行。
  • 一些設備管理器通常會被設置爲單例模式,比如一個電腦有兩臺打印機,在輸出的時候就要處理,不能兩臺打印機打印同一個文件。
  • 餓了要吃飯也是一個單例,不同菜系的菜館都可以滿足我填飽肚子的需求,在真的走進菜館吃飯的話,不能同時去多個菜館消費填飽肚子。

優點

  • 1、內存只有一個實例,減少內存開銷,尤其頻繁創建和銷燬實例(比如頁面緩存)。
  • 2、避免對資源的多重佔用(比如寫文件操作)。

使用場景

  • 1、要求生產唯一序列號。
  • 2、WEB中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來。
  • 3、創建的一個對象需要消耗的資源過多,比如 I/O 與數據庫的連接等。

注意事項:getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入造成 instance 被多次實例化。

代碼

GitHub —— Singleton 單例模式

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