手撕設計模式之「單例模式」(詳細解析)

前言

單例模式主要用來保證系統中某個類的實例對象的唯一性,是最簡單的一種設計模式,而且在面試中也經常會被問到,是非常值得我們去學習的。如果你們面試遇到了哪些設計模式的考察,也歡迎留言,我會及時發新的博文。

1. 模式定義

單例模式(Singleton Pattern):確保某一個類只有一個實例,而且自行實例化並像整個系統提供這個實例,這個類稱爲單例類,它會提供全局訪問的方法。單例模式是對象創建型模式的一種。

單例模式的三個要點:

  • 某個類只能有一個實例
  • 必須自行創建這個實例
  • 必須自行向整個系統提供這個實例

2. 模式實現

要想實現單例模式,我們可以從它的三個要點入手:

首先是單例的唯一性,既然單例是唯一的,我們毫無疑問應該給他加上 static 關鍵字,又因爲這個對象不應該直接暴露,所以還要加上 private 進行訪問限定。

private static Singleton instince = null;

接着,就是這個對象需要由單例類自行創建,這時我們就應該屏蔽外界訪問該類初始化方法的接口,也就是用 private 修飾構造函數。

private Singleton() {
}

最後一點就比較容易實現了,給公有工廠方法 static 關鍵字,即可實現向整個系統提供這個實例。

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

將以上代碼整合一下,就可以得到如下的單例模式的代碼實現模板:

public class Singleton {
	//靜態私有成員變量,保證實例的唯一性
	private static Singleton instince = null;
	//私有構造函數,保證實例由類自行創建
	private Singleton() {
	}
	// 靜態公有工廠方法,向系統提供這個唯一實例
	public static Singleton getInstance() {
		if (instance == null) {
			instince = new Singleton();
		}
		return instance;
	}
}

下面是針對上面單例模式實現模板的客戶端測試代碼:

public Client {
	public static void main(String []args) {
		Singleton s1 = Singleton.getInstance();
		Singleton s2 = Singleton.getInstance();
		System.out.println(s1 == s2); //true
	}
}

3. 單例模式的拓展

單例模式可以分爲懶漢式單例餓漢式單例,它們的區別主要在單例類對象的初始化時間上。下面我們來詳細講解:

3.1 懶漢式單例

懶漢式單例模式的結構圖如下圖所示:
在這裏插入圖片描述
在模式圖中,我們可以看到這實例對象在創建類的時候並沒有初始化,而是等到有人調用getInstance()方法獲取實例的時候才進行實例對象的初始化,這也恰恰體現了懶漢式的這種“懶惰行爲”。

下面是以“身份證號碼類”作爲例子,編寫的懶漢式單例代碼。

public class IdentityCardNo {
	private String no;
	
	//某個類只能有一個實例
	private static IdentityCardNo instince = null;
	
	//必須自行創建這個實例
	private IdentityCardNo() {
		
	}
	
	//必須自行向整個系統提供這個實例
	public static IdentityCardNo getInstance() {
		if (instince == null) {
			System.out.println("第一次辦理身份證,分配新號碼");
			instince = new IdentityCardNo();
			instince.setIdentityCardNo("400000199710301111");
		} else {
			System.out.println("重複辦理身份證,獲取舊號碼");
		}
		return instince;
	}
	public String getIdentityCardNo() {
		return no;
	}

	public void setIdentityCardNo(String no) {
		this.no = no;
	}
}

可以仿造上面的方法,寫一個Client類來進行檢驗。

3.2 餓漢式單例

餓漢式單例模式的結構圖如下圖所示:
在這裏插入圖片描述
由於餓漢式單例的“飢餓”特性,使得它在類加載階段就對單例類對象進行了初始化,從上面的結構圖也可以看出這一點。

下面代碼是上面那個例子的餓漢式實現:

public class IdentityCardNo {
	private String no;
	
	//某個類只能有一個實例
	private static IdentityCardNo instince = new IdentityCardNo();
	
	//必須自行創建這個實例
	private IdentityCardNo() {
		System.out.println("第一次辦理身份證,分配新號碼");
	}
	
	//必須自行向整個系統提供這個實例
	public static IdentityCardNo getInstance() {
		instince.setIdentityCardNo("400000199710301111");
		return instince;
	}
	public String getIdentityCardNo() {
		return no;
	}

	public void setIdentityCardNo(String no) {
		this.no = no;
	}
}

4. 多線程環境中的單例模式

上面提到的懶漢式單例是線程不安全的,在多線程的環境下,對象的唯一性得不到保障。於是就有了下面幾種線程安全的單例模式實現方法,其中第1,2種方法是對懶漢式的改進,第 3 中是對餓漢式的改進。

4.1 延時加載

我們可以用synchronized關鍵字修飾getInstance()方法,利用延時加載的方式保證在多線程環境下對象的唯一性。但是這種方法在容易造成線程擁塞,效率不高。

實現代碼如下:

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

4.2 雙重校驗鎖

這種方式採用雙鎖機制,線程安全且在多線程情況下能保持高性能,是比較推薦使用的方法。

實現代碼如下:

public class Singleton {
	private static Singleton instince = null;
	private Singleton() {
	}
	public static Singleton getInstance() {
		if (instance == null) {
			//只將synchronized關鍵字用在了初始化模塊
			synchronized (Singleton.class) {
				if (install == null) {
					instince = new Singleton();
				}
			}
		}
		return instance;
	}
}

4.3 靜態內部類

這種方式通過給對象加上final關鍵字修飾,能達到雙檢鎖方式一樣的功效,但實現更簡單。對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用於靜態域的情況,雙檢鎖方式可在實例域需要延遲初始化時使用。

實現代碼如下:

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

參考資料

  1. 單例模式| 菜鳥教程
  2. 《Java設計模式》
  3. Java實現單例模式 | CSDN
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章