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