Singleton Pattern 單例模式,作為創建型模式的一種,其保證了類的實例對象只有一個,並對外提供此唯一實例的訪問介面 概述 對於單例模式而言,其最核心的目的就是為了保證該類的實例對象是唯一的。為此一方面,需要將該類的構造函數設為private,另一方面,該類需要在內部完成實例的構造並對外 ...
Singleton Pattern 單例模式,作為創建型模式的一種,其保證了類的實例對象只有一個,並對外提供此唯一實例的訪問介面
概述
對於單例模式而言,其最核心的目的就是為了保證該類的實例對象是唯一的。為此一方面,需要將該類的構造函數設為private,另一方面,該類需要在內部完成實例的構造並對外提供訪問介面。單例模式的好處顯而易見,可以避免頻繁創建、銷毀實例所帶來的性能開銷;但其缺點也同樣明顯,此類不僅需要描述業務邏輯,同時還需要構造出該類的唯一對象並對外提供訪問介面,其顯然違背了單一職責原則
實現
單例模式的思想雖然簡單易懂,但實現起來卻可謂是花樣繁多、妙不可言。這裡來介紹幾種常見的單例模式的實現
餓漢式
如下實現最為簡單,當 SingletonDemo1 類被載入到JVM中,即會完成實例化。即不是所謂的Lazy Load 延遲載入,故通常被稱之為 “餓漢式” 單例。其最大的問題就在,可能構造出來的實例對象從頭到尾沒有被使用過(沒有調用過getInstance方法),從而浪費記憶體。可能有人會對此有些困惑,SingletonDemo1 類被載入到JVM中了,那肯定是因為調用了getInstance方法啊。難道還有別的原因?答案是肯定的
這裡,我們先簡要補充一些類載入機制的相關知識點。我們知道Java中的類被載入到JVM中,通常會有如下幾個階段:載入、 驗證、準備、解析、初始化等。其中對於初始化階段而言,虛擬機規範嚴格規定了有且僅有以下5種情況必須立即對類進行初始化(而載入、 驗證、準備顯然必須在此之前開始):
- 遇到new、getstatic、putstatic或invokestatic類型的位元組碼指令時,在Java代碼層面上就是new對象、讀取或設置類的靜態變數(被final修飾、已在編譯期將結果放入常量池的靜態變數除外)、調用類的靜態方法
- 對該類使用反射
- 當初始化一個類的時候,如果發現其父類還未初始化,則需要先初始化父類
- 當JVM啟動時,虛擬機會先初始化開發者所指定的主類(即main方法所在類)
- 當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後解析的結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且該方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化
說到這裡,大家可能就明白了,如果SingletonDemo1類中還有其他靜態方法,一旦被調用就會導致SingletonDemo1類被載入、初始化,此時即完成了實例的構造。眾所周知,JVM保證了類載入過程的線程安全,所以餓漢式單例同樣是線程安全的
/**
* 單例模式1,餓漢式
*/
public class SingletonDemo1 {
private static SingletonDemo1 instance = new SingletonDemo1("我是餓漢式的單例");
private String description;
/**
* 私有構造器
* @param description
*/
private SingletonDemo1(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
/**
* 提供實例的訪問介面
* @return
*/
public static SingletonDemo1 getInstance() {
return instance;
}
public static void main(String[] args) {
SingletonDemo1 singletonDemo1 = SingletonDemo1.getInstance();
singletonDemo1.getInfo();
}
}
測試結果如下所示
懶漢式
前面說到,餓漢式單例會導致記憶體空間的浪費,那麼有沒有辦法解決這個問題呢?答案是有的,這就是”懶漢式”單例。顧名思義,其實例不是在類載入、初始化時被構建的,而是在真正需要的時候才去創建,如下所示
/**
* 單例模式2,線程不安全的懶漢式
*/
public class SingletonDemo2 {
private static SingletonDemo2 instance = null;
private String description;
private SingletonDemo2(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
public static SingletonDemo2 getInstance() {
if( instance==null ) {
instance = new SingletonDemo2("我是線程不安全的懶漢式單例");
}
return instance;
}
public static void main(String[] args) {
SingletonDemo2 singletonDemo2 = SingletonDemo2.getInstance();
singletonDemo2.getInfo();
}
}
測試結果如下所示
“懶漢式”單例雖然實現了Lazy Load延遲載入,但是其存在一個很嚴重的問題,不是線程安全的。所以如果在多線程環境下,我們需要使用下麵線程安全的”懶漢式”單例,其保障線程安全的手段也很簡單,直接使用synchronized來修飾getInstance方法。這種辦法過於簡單粗暴,同時會導致效率十分低下。實例一旦被構造完畢後,由於鎖的存在,導致每次只能由一個線程可以獲取到實例對象
/**
* 單例模式3, 線程安全但效率低下的懶漢式
*/
public class SingletonDemo3 {
private static SingletonDemo3 intance = null;
private String description;
private SingletonDemo3(String description) {
this.description = description;
}
public void getInfo() {
System.out.printf(description);
}
public static synchronized SingletonDemo3 getInstance() {
if( intance==null ) {
intance = new SingletonDemo3("我是線程安全線程安全但效率低下的懶漢式單例");
}
return intance;
}
public static void main(String[] args) {
SingletonDemo3 singletonDemo3 = SingletonDemo3.getInstance();
singletonDemo3.getInfo();
}
}
測試結果如下所示
基於DCL(Double-Checked Locking)雙重檢查鎖的單例
通過前面我們看到,無論是餓漢式單例還是懶漢式單例,其都有明顯的缺點。那麼有沒有一種完美的單例?既可以實現Lazy Load延遲載入,又可以在保證線程安全的前提下依然具備較高的效率呢。答案是肯定——基於DCL(Double-Checked Locking)雙重檢查鎖的單例。其實現如下,該單例實現中進行了兩次檢查。第一次檢查時如果發現實例已經構造完畢了,則無需加鎖直接返回實例對象即可。其保證了實例在構建完成後,其他多個線程可以同時快速獲取該實例。第二次檢查時則是為了避免重覆構造實例,因為在還未構造實例前,可能會有多個線程通過了第一次檢查,準備加鎖來構造實例。在DCL的單例實現中,尤其需要註意的一點是靜態變數instance必須要使用volatile進行修飾。其原因在於volatile禁止了指令的重排序。這裡就此問題再作一些詳細的解釋說明:在JDK1.5之前的Java記憶體模型中,雖然不允許volatile變數之間進行重排序,但卻允許普通變數與volatile變數之間的重排序。所以在JSR 133(JDK 1.5)中對volatile變數的記憶體語義進一步增強,即限制了普通變數與volatile變數之間是否可以重排序的具體場景。這也是為什麼在JDK 1.5之前無法通過DCL實現一個線程安全的單例模式
/**
* 單例模式4,基於DCL的線程安全的單例
*/
public class SingletonDemo4 {
// 此處必須要使用volatile修飾!
private static volatile SingletonDemo4 instance = null;
private String description;
private SingletonDemo4(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
public static SingletonDemo4 getInstance() {
if( instance==null ) { // 第一次檢查:如果實例已經構造完成則直接取,避免每次取之前需要獲取鎖
synchronized (SingletonDemo4.class) {
if(instance==null) { // 第二次檢查:避免構造出多個實例
instance = new SingletonDemo4("我是基於DCL的線程安全的單例");
}
}
}
return instance;
}
public static void main(String[] args) {
SingletonDemo4 singletonDemo4 = SingletonDemo4.getInstance();
singletonDemo4.getInfo();
}
}
測試結果如下
基於靜態內部類的單例
前面我們說到的第一種單例實現,之所以被稱為餓漢式、非延遲載入。其原因就在於類的載入、初始化不能100%保證是因為調用getInstance方法引起的。而這裡我們通過靜態內部類的方式來實現一個延遲載入的單例,代碼如下所示。當調用外部類SingletonDemo5的一些靜態方法(當然getInstance方法除外),只會載入、初始化外部類SingletonDemo5,而不會去初始化靜態內部類SingletonDemo5Holder。只有通過調用getInstance方法訪問了靜態內部類SingletonDemo5Holder的靜態變數instance,靜態內部類SingletonDemo5Holder才會被載入、初始化,顯然此時實例才會被真正的構造。所以對於基於靜態內部類的單例實現而言,其之所以能保證Lazy Load延遲載入特性,是其因為通過SingletonDemo5Holder靜態內部類100%保證了靜態內部類被載入、初始化是因為調用外部類的getInstance方法而導致的。同樣地,該方式的單例也是滿足線程安全的,原因在餓漢式單例實現中已作解釋,此處就不再贅述
/**
* 單例模式5,靜態內部類
*/
public class SingletonDemo5 {
private String description;
private SingletonDemo5(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
private static class SingletonDemo5Holder{
private static final SingletonDemo5 instance = new SingletonDemo5("我是基於靜態內部類的線程安全的單例");
}
public static SingletonDemo5 getInstance() {
return SingletonDemo5Holder.instance;
}
public static void main(String[] args) {
SingletonDemo5 singletonDemo5 = SingletonDemo5.getInstance();
singletonDemo5.getInfo();
}
}
測試結果如下所示
基於枚舉的單例
對於Java的枚舉類型而言,其構造器是且只能是private私有的。故其特別適合用於實現單例模式。下麵即是一個基於枚舉的單例實現,可以看到此種實現非常簡潔優雅。當枚舉類進行載入、初始化時,即會完成實例的構建,我們通過枚舉的特性保證了實例的唯一性,當然其不是Lazy Load延遲載入的。與此同時根據類的載入機制我們可知其也是線程安全的(由JVM保證)
/**
* 單例模式6,枚舉法
*/
public enum SingletonDemo6 {
INSTANCE("我是枚舉法的單例");
private String description;
/**
* 枚舉的構造器預設訪問許可權是private, 當然也只能是私有的
* @param description
*/
SingletonDemo6(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
}
...
/**
* 測試用例
*/
public class SingletonDemo6Test {
public static void main(String[] args) {
SingletonDemo6 singletonDemo6 = SingletonDemo6.INSTANCE;
singletonDemo6.getInfo();
}
}
測試結果如下
參考文獻
- Head First 設計模式 弗里曼著