大話設計模式,創建型模式之單例模式

模式動機與定義

模式動機

對於系統中的某些類來說,只有一個實例很重要。例如,一個系統中可以存在多個打印任務,但是只能有一
個正在工作的任務;一個系統只能有一個窗口管理器或文件系統;一個系統只能有一個計時工具或ID(序
號)生成器。
如何保證一個類只有一個實例並且這個實例易於被訪問呢?定義一個全局變量可以確保對象隨時都可以被訪
問,但不能防止我們實例化多個對象。一個更好的解決辦法是讓類自身負責保存它的唯一實例。這個類可以
保證沒有其他實例被創建,並且它可以提供一個訪問該實例的方法。這就是單例模式的模式動機。

定義

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

  • 一是某個類只能有一個實例;
  • 二是它必須自行創建這個實例;
  • 三是它必須自行向整個系統提供這個實例。

單例模式是一種對象創建型模式。單例模式又名單件模式或單態模式。

模式結構與分析

模式結構

單例模式是結構最簡單的設計模式一,在它的核心結構中只包含一個被稱爲單例類的特殊類。單例模式結構
如圖所示:
在這裏插入圖片描述
單例模式包含如下角色:Singleton:單例

模式分析

單例模式的目的是保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。單例模式包含的角色只有一
個,就是單例類-------Singleton。單例類擁有一個私有構造函數,確保用戶無法通過new關鍵字直接實例化
它。除此之外,該模式中包含一個靜態私有成員變量與靜態公有的工廠方法,該工廠方法負責檢驗實例的存
在性並實例化自己,然後存儲在靜態成員變量中,以確保只有一個實例被創建。

單例模式的實現代碼如下所示:

public class Singleton {
	private static Singleton instance = null; // 靜態私有成員變量
	// 私有構造函數

	private Singleton() {
	}

	// 靜態公有工廠方法,返回唯一實例
	public static Singleton getInstance() {
		if (instance == null)
			instance = new Singleton();
		return instance;
	}

}

在單例模式的實現過程中,需要注意以下三點:

  • 單例類的構造函數爲私有
  • 提供一個自身的靜態私有成員變量
  • 提供一個公有的靜態工廠方法

模式實例與解析

模式實例與解析

實例一:身份證號碼

在現實生活中,居民身份證號碼具有唯一性,同一個人不允許有多個身份證號碼,第一次申請身份證時將給
居民分配一個身份證號碼,如果之後因爲遺失等原因補辦時,還是原來的身份證號碼,不會產生新的號碼。
使用單例模式模擬該場景。
在這裏插入圖片描述
參考代碼如下:

public class IdetntityCardNo {
	private static IdetntityCardNo instance = null;
	private String no;

	private IdetntityCardNo() {
	}

	public static IdetntityCardNo getInstance() {
		if (instance == null) {
			System.out.println("第一次辦理身份證,分配新號碼");
			instance = new IdetntityCardNo();
			instance.setIdetntityCardNo("NO410221199001013822");
		} else {
			System.out.println("重複辦理身份證,獲取舊號碼");
		}
		return instance;
	}

	private void setIdetntityCardNo(String no) {
		this.no = no;
	}

	String getIdentityCardNo() {
		return this.no;
	}

}
實例二:打印池

在操作系統中,打印池(Print Spooler)是一個用於管理打印任務的應用程序,通過打印池用戶可以刪除、中
止或者改變打印任務的優先級,在一個系統中只允許運行一個打印池對象,如果重複創建打印池則拋出異
常。現使用單例模式來模擬實現打印池的設計。
在這裏插入圖片描述
參考代碼如下:

  1. PrintSpoolerSinglenton類:
public class PrintSpoolerSinglenton {
	private static PrintSpoolerSinglenton instance = null;

	private PrintSpoolerSinglenton() {
	}

	public static PrintSpoolerSinglenton getInstance() {
		if (instance == null) {
			System.out.println("創建打印池!");
			instance = new PrintSpoolerSinglenton();
		} else {
			System.out.println("打印池正在工作中!");
			return null;
		}
		return instance;
	}

	public void manageJobs() {
		System.out.println("管理打印事務!");
	}
}

  1. PrintSpoolerException 類:
public class PrintSpoolerException extends Exception {
	public PrintSpoolerException(String message) {
		super(message);
	}
}
  1. Cllient 類:
public class Cllient {
	public static void main(String[] arg) {
		PrintSpoolerSinglenton ps1, ps2;
		try {
			ps1 = PrintSpoolerSinglenton.getInstance();
			ps1.manageJobs();
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
		System.out.println("----------------------------");
		try {
			ps2 = PrintSpoolerSinglenton.getInstance();
			ps2.manageJobs();
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}
}

模式效果與應用

模式效果

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

模式應用

在以下情況下可以使用單例模式:

  • 系統只需要一個實例對象,如系統要求提供一個唯一的序列號生成器,或者需要考慮資源消耗太大而只 允許創建一個對象。
  • 客戶調用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實 例。
  • 在一個系統中要求一個類只有一個實例時才應當使用單例模式。反過來,如果一個類可以有幾個實例共
    存,就需要對單例模式進行改進,使之成爲多例模式。
  1. java.lang.Runtime類
public class Runtime {
	private static Runtime currentRuntime = new Runtime();

	public static Runtime getRuntime() {
		return currentRuntime;
	}

private Runtime(){}......
}

  1. 一個具有自動編號主鍵的表可以有多個用戶同時使用,但數據庫中只能有一個地方分配下一個主鍵編
    號,否則會出現主鍵重複,因此該主鍵編號生成器必須具備唯一性,可以通過單例模式來實現。
  2. 默認情況下,Spring會通過單例模式創建bean

模式擴展

懶漢式,線程不安全

是否 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 才能保證單例,但加鎖會影響效率。 getInstance() 的性能對應用程序不是很關鍵(該方法使用不太頻繁)。

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

餓漢式

是否 Lazy 初始化:

是否多線程安全:

實現難度:

描述:這種方式比較常用,但容易產生垃圾對象。

優點:沒有加鎖,執行效率會提高。

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

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

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

JDK 版本:JDK1.5 起

是否 Lazy 初始化:

是否多線程安全:

實現難度:較複雜

描述:這種方式採用雙鎖機制,安全且在多線程情況下能保持高性能。 getInstance() 的性能對應用程序很關鍵。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}

登記式/靜態內部類

是否 Lazy 初始化:

是否多線程安全:

實現難度:一般

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

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

枚舉

JDK 版本:JDK1.5 起

是否 Lazy 初始化:

是否多線程安全:

實現難度:

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

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

經驗之談

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

發佈了48 篇原創文章 · 獲贊 29 · 訪問量 2565
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章