單例模式(Singleton Pattern)

思思問問發表於2020-12-05

在我們的系統中,有一些物件其實我們只需要一個,比如說:執行緒池、快取、對話方塊、登錄檔、日誌物件、充當印表機、顯示卡等裝置驅動程式的物件。事實上,這一類物件只能有一個例項,如果製造出多個例項就可能會導致一些問題的產生,比如:程式的行為異常、資源使用過量、或者不一致性的結果。

如何保證一個類只有一個例項並且這個例項易於被訪問呢?定義一個全域性變數可以確保物件隨時都可以被訪問,但不能防止我們例項化多個物件。

更好的解決辦法是讓類自身負責儲存它的唯一例項。這個類可以保證沒有其他例項被建立,並且它可以提供一個訪問該例項的方法。

定義

單例模式(Singleton Pattern):單例模式確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項,這個類稱為單例類,它提供全域性訪問的方法。

J2EE 標準中的 ServletContext 和 ServletContextConfig、Spring框架應用中的 ApplicationContext、資料庫中的連線池等也都是單例模式。

單例模式的要點有三個:

  • 單例類只能有一個例項物件;
  • 該單例物件必須由單例類自行建立;
  • 單例類對外提供一個訪問該單例的全域性訪問點。

結構

單例模式的主要角色如下。

  • 單例類:包含一個例項且能自行建立這個例項的類。
  • 訪問類:使用單例的類。

以懶漢式為例,UML類圖如下:

單例模式(Singleton Pattern)

時序圖如下:

單例模式(Singleton Pattern)

實現

Ⅰ餓漢式-靜態常量

public class Singleton {
    // 構造器私有化,使用者無法通過new方法建立該物件例項
    private Singleton() {
    }
    
    // 在靜態初始化器中建立單例例項,這段程式碼保證了執行緒安全
    private static Singleton uniqueInstance = new Singleton();

    // 提供一個公有的靜態方法,返回例項物件
    public static Singleton getInstance() {
        return uniqueInstance;
    }
}

優點:這種寫法比較簡單,在類裝載的時候就完成例項化,避免了執行緒同步等問題,是執行緒安全的

缺點:在類裝載的時候就完成例項化,沒有達到懶載入(Lazy Loading)的效果。如果從始至終從未使用過這個例項,則會造成記憶體浪費

這種單例模式可用,但可能造成記憶體浪費。

Ⅱ 餓漢式-靜態程式碼塊

public class Singleton {
    private Singleton() {}

    private static Singleton uniqueInstance;

    // 靜態程式碼塊中建立例項
    static {
        uniqueInstance = new Singleton();
    }
    
    public static Singleton getInstance() {
        return uniqueInstance;
    }
}

與上面類似,也是在類裝載的時候就完成例項化,只不過將類例項化的過程放在了靜態程式碼塊中。

這種單例模式可用,但可能造成記憶體浪費。

Ⅲ 懶漢式-執行緒不安全

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }
    // 提供一個靜態的公有方法,當使用到該方法時,才去建立uniqueInstance
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

優點:起到了**懶載入(Lazy Loading)**的效果,如果沒有用到該類,那麼就不會例項化 uniqueInstance,從而節約資源。

缺點:執行緒不安全。如果多個執行緒能夠同時進入 if (uniqueInstance == null) ,並且此時 uniqueInstance 為 null,那麼會有多個執行緒執行 uniqueInstance = new Singleton(); 語句,這將導致例項化多次 uniqueInstance

實際開發中,不要使用。

Ⅳ 懶漢式-執行緒安全

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }
    // 提供一個靜態的公有方法,當使用到該方法時,才去建立uniqueInstance
    public static synchronized Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

優點:執行緒安全。只需要對 getUniqueInstance() 方法加鎖,那麼在一個時間點只能有一個執行緒能夠進入該方法,從而避免了例項化多次 uniqueInstance。

缺點:效率低。每個執行緒在想獲得類的例項時候,執行 getInstance()方法都要先進行同步,即使 uniqueInstance 已經被例項化了,這會讓執行緒阻塞時間過長。

實際開發中,不推薦使用。

Ⅴ 懶漢式-執行緒安全-雙重校驗鎖

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }
	//提供一個靜態的公有方法,加入雙重檢查程式碼,解決執行緒安全問題, 同時解決懶載入問題
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {// 避免已經例項化後的加鎖操作
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {// 避免多個執行緒同時進行例項化操作
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

**雙重校驗鎖(Double-Check Locking)**概念是多執行緒開發中常使用到的。假如在uniqueInstance == null的情況下,兩個執行緒都執行了 if 語句,那麼兩個執行緒都會進入 if 語句塊內。可以肯定都是,兩個執行緒都會執行 uniqueInstance = new Singleton(); 這條語句,只是先後的問題,也就是說肯定會有兩次例項化。

因此必須需要使用兩個 if 語句:第一個 if 語句用來避免 uniqueInstance 已經被例項化之後的加鎖操作;第二個 if 語句進行了加鎖,只允許一個執行緒進入,保證了執行緒安全,避免出現uniqueInstance == null時兩個執行緒同時進行例項化操作問題。

uniqueInstance採用 volatile關鍵字修飾也是很有必要的,因為 JVM了效能優化,可能會指令重排。指令重排在單執行緒環境下不會出現問題,但是在多執行緒環境下會導致一個執行緒獲得還沒有初始化的例項。而使用 volatile 修飾uniqueInstance後會引入記憶體屏障(Memory Barrier),保證了JVM的可見性與有序性。(雙重校驗鎖詳解可參考之前寫的【併發程式設計】volatile一文)

總之,雙重檢驗鎖保證了執行緒安全,同時懶漢式保證了延遲載入。

效率較高,推薦使用。

Ⅵ 靜態內部類

class Singleton {
    //構造器私有化
    private Singleton() {
    }

	// 靜態內部類,該類中有一個靜態屬性 Singleton
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

	// 提供一個靜態的公有方法,直接返回 SingletonInstance.INSTANCE
    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

當 Singleton 類被載入時,靜態內部類 SingletonHolder 沒有被載入進記憶體。只有當呼叫 getUniqueInstance() 方法從而觸發 SingletonHolder.INSTANCE 時 SingletonHolder 才會被載入,此時初始化 INSTANCE 例項,並且 JVM 能確保 INSTANCE 只被例項化一次。

這種方式不僅具有延遲初始化的好處,利用靜態內部類特點實現了延遲載入,而且由 JVM 提供了對執行緒安全的支援。

效率高,推薦使用。

VII 列舉

enum Singleton {
    INSTANCE; //屬性

    public void doSomeTing() {
        System.out.println("通過列舉方法實現單例");
    }
}

使用:

public class EnumSingletonTest {

	public static void main(String[] args) {
		Singleton singleton = Singleton.INSTANCE;
		singleton.doSomeThing();// 通過列舉方法實現單例
	}
}

這種方式是Effective Java 作者Josh Bloch 提倡的方式。雖然這種方法還沒有廣泛採用,但是單元素的列舉型別已經成為實現Singleton的最佳方法。這種方法在功能上與公有域方法相近,但是它更加簡潔,無償提供了序列化機制,絕對防止多次例項化,即使是在面對複雜序列化或者反射攻擊的時候。詳細分析可參考Java單例模式的7種寫法中,為何用Enum列舉實現被認為是最好的方式一文。

推薦使用。

JDK

總結

優點

  • 提供了對唯一例項的受控訪問。
  • 由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件,單例模式無疑可以提高系統的效能。
  • 允許可變數目的例項。我們可以基於單例模式進行擴充套件,使用與單例控制相似的方法來獲得指定個數的物件例項。

缺點

  • 由於單例模式中沒有抽象層,因此單例類的擴充套件有很大的困難
  • 單例類的職責過重,在一定程度上違背了“單一職責原則”。因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的建立和產品的本身的功能融合到一起。
  • 濫用單例將帶來一些負面問題,如為了節省資源將資料庫連線池物件設計為單例類,可能會導致共享連線池物件的程式過多而出現連線池溢位;Java、執行環境中提供了自動垃圾回收的技術,如果例項化的物件長時間不被利用,系統會認為它是垃圾,會自動銷燬並回收資源,下次利用時又將重新例項化,這會導致物件狀態的丟失。

適用場景

需要頻繁的進行建立和銷燬、或者建立時耗時過多或耗費資源過多但又經常用到的物件;工具類物件;頻繁訪問資料庫或檔案的物件(比如資料來源、session工廠等)。

參考

相關文章