單例設計模式詳解

單例設計模式詳解

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

模式定義

單例模式(Singleton Pattern):單例模式確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。單例模式的要點有三個:一是某個類只能有一個實例;二是它必須自行創建這個實例;三是它必須自行向整個系統提供這個實例。單例模式是一種對象創建型模式。單例模式又名單件模式或單態模式。

  • Singleton:單例
../_images/Singleton.jpg

時序圖

../_images/seq_Singleton.jpg

下面就算單例的5種實現方式:

1)懶漢式,線程安全

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

雖然做到了線程安全,並且解決了多實例的問題,但是它並不高效。因爲在任何時候只能有一個線程調用 getInstance() 方法。但是同步操作只需要在第一次調用時才被需要,即第一次創建單例實例對象時。

2)雙重檢索

public static Singleton getSingleton(){
if (instance == null) {                         //Single Checked
synchronized (Singleton.class) {
if (instance == null) {                 //Double Checked
                instance = new Singleton();
            }
        }
    }
return instance ;
}

是一種使用同步塊加鎖的方法。因爲會有兩次檢查instance == null,一次是在同步塊外,一次是在同步塊內。在同步塊內還要再檢驗一次,是因爲可能會有多個線程一起進入同步塊外的 if,如果在同步塊內不進行二次檢驗的話就會生成多個實例了。

這段代碼看起來很完美,但還是有問題。主要在於instance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。

  1. 給 instance 分配內存
  2. 調用 Singleton 的構造函數來初始化成員變量
  3. 將instance對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)

但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回instance,然後使用就會報錯。

只需要將 instance 變量聲明成 volatile就可以了。

public class Singleton {
private volatile static Singleton instance; //聲明成 volatile
private Singleton(){}
public static Singleton getSingleton(){
<span style="white-space:pre">	</span>if (instance == null) {                         
<span style="white-space:pre">		</span>synchronized (Singleton.class) {
<span style="white-space:pre">		</span>if (instance == null) {       
                    instance = new Singleton();
                }
            }
        }
<span style="white-space:pre">	</span>return instance;
    }
}

使用 volatile 的主要原因是其一個特性:禁止指令重排序優化。在 volatile 變量的賦值操作後面會有一個內存屏障(生成的彙編代碼上),讀操作不會被重排序到內存屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變量的寫操作都先行發生於後面對這個變量的讀操作但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。

3)餓漢式 static final field

public class Singleton{
	//類加載時就初始化
	private static final Singleton instance = new Singleton();
	private Singleton(){}
	public static Singleton getInstance(){
		return instance;
    	}
}

缺點是它不是一種懶加載模式(lazy initialization),單例會在加載類後一開始就被初始化,即使客戶端沒有調用 getInstance()方法。
4)靜態內部類 static nested Class
public class Singleton {  
private static class SingletonHolder {  
	private static final Singleton INSTANCE = new Singleton();  
    }  
private Singleton(){}  
public static final Singleton getInstance(){  
	return SingletonHolder.INSTANCE; 
    }  
}
這種寫法仍然使用JVM本身機制保證了線程安全問題;由於 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本這是一種可以採取的較優良的實現方式。

5)枚舉
public enum EasySingleton{
    INSTANCE;
}
通過EasySingleton.INSTANCE來訪問實例,這比調用getInstance()方法簡單多了。創建枚舉默認就是線程安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新創建新的對象
一般情況下直接使用餓漢式就好了,如果明確要求要懶加載(lazy initialization)可以使用靜態內部類,如果涉及到反序列化創建對象時可以試着使用枚舉的方式來實現單例。

總結:

  • 單例模式確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。單例模式的要點有三個:一是某個類只能有一個實例;二是它必須自行創建這個實例;三是它必須自行向整個系統提供這個實例。單例模式是一種對象創建型模式。
  • 單例模式只包含一個單例角色:在單例類的內部實現只生成一個實例,同時它提供一個靜態的工廠方法,讓客戶可以使用它的唯一實例;爲了防止在外部對其實例化,將其構造函數設計爲私有。
  • 單例模式的目的是保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。單例類擁有一個私有構造函數,確保用戶無法通過new關鍵字直接實例化它。除此之外,該模式中包含一個靜態私有成員變量與靜態公有的工廠方法。該工廠方法負責檢驗實例的存在性並實例化自己,然後存儲在靜態成員變量中,以確保只有一個實例被創建。
  • 單例模式的主要優點在於提供了對唯一實例的受控訪問並可以節約系統資源;其主要缺點在於因爲缺少抽象層而難以擴展,且單例類職責過重。
  • 單例模式適用情況包括:系統只需要一個實例對象;客戶調用類的單個實例只允許使用一個公共訪問點。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章