設計理念
在有些系統中,爲了節省內存資源、保證數據內容的一致性,對某些類要求只能創建一個實例,這就是所謂的單例模式。
單例模式,是23種設計模式中使用最廣泛的一種設計模式,同是也是最重要的設計模式之一。
定義
單例(Singleton)模式的定義:指一個類只有一個實例,且該類能自行創建這個實例的一種模式。
例如,Windows 中只能打開一個任務管理器,這樣可以避免因打開多個任務管理器窗口而造成內存資源的浪費,或出現各個窗口顯示內容的不一致等錯誤。
在計算機系統中,還有 Windows 的回收站、操作系統中的文件系統、多線程中的線程池、顯卡的驅動程序對象、打印機的後臺處理服務、應用程序的日誌對象、數據庫的連接池、網站的計數器、Web 應用的配置對象、應用程序中的對話框、系統中的緩存等常常被設計成單例。
特點
單例模式有 3 個特點:
- 單例類只有一個實例對象;
- 該單例對象必須由單例類自行創建;
- 單例類對外提供一個訪問該單例的全局訪問點;
結構與實現
單例模式是設計模式中最簡單的模式之一。通常,普通類的構造函數是公有的,外部類可以通過“new 構造函數()”來生成多個實例。但是,如果將類的構造函數設爲私有的,外部類就無法調用該構造函數,也就無法生成多個實例。這時該類自身必須定義一個靜態私有實例,並向外提供一個靜態的公有函數用於創建或獲取該靜態私有實例。
結構
單例模式的主要角色如下。
- 單例類:包含一個實例且能自行創建這個實例的類。
- 訪問類:使用單例的類。
其結構如圖 1 所示。
實現
單例模式最常見的有6種實現方式,每種方式都有其特點,都值得大家去仔細推敲。
-
懶漢式,線程不安全
代碼實例:
package com.design.pattern.creationalPattern.singletonPattern;
public class Singleton {
//Q:這裏可不可以去掉static?
//A:不可以,因爲getInstance()方法是靜態方法,
//不可以在靜態方法裏調用非靜態變量
private static Singleton instance;
private Singleton(){
//構造器私有化,防止外部訪問,通過new Singleton()方式創建對象
}
/**
* Q:如果多個線程同時訪問,會怎樣?
* A:嚴格意義上,這種方式不能算真正意義上的單例。
* 當線程併發,線程上下文切換時,
* 假設 有線程一和線程二。
* 線程一 通過 if(instance == null )後,
* 在 nstance = new Singleton(); 被掛起
* 線程二 正常執行完了整個getInstance()後,
* 線程一獲得線程使用權。
* 這時候線程一拿到的instance實例,就不是之前的實例對象了。
* */
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
對於它線程不安全的情況,我們做個試驗來驗證。
package com.design.pattern.creationalPattern.singletonPattern;
public class UnSafeSingletonTest {
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Singleton instance1 = Singleton.getInstance();
System.out.println(instance1);
}
});
thread1.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Singleton instance2 = Singleton.getInstance();//在這裏打上debug斷點,當斷點到達這個位置時,在Singleton#getInstance()方法中instance = new Singleton(),打上斷點
System.out.println(instance2);
}
});
thread2.start();
}
}
模擬線程上下文切換,這裏我模擬休眠線程,將斷點位置先打到了Singleton 類的instance = new Singleton()位置。
- 當線程一過來的時候,直接放開斷點,可以看到線程1和線程2同時到達,如圖一。
- 當線程二過來的時候,在debug watches裏讓線程二休眠2毫秒,模擬線程上下文切換場景。如圖二
- 再放開斷點,可以明顯的看到線程一和線程二都去創建了實例對象,並且線程一現在持有的引用是線程二創建的對象地址。這時候,線程一再去使用instance就可能出現問題,得到結果如下:
SingletonThread2 SingletonThread1
com.design.pattern.creationalPattern.singletonPattern.Singleton@17ceefc4
com.design.pattern.creationalPattern.singletonPattern.Singleton@1d4331b
- 懶漢式,線程安全
package com.design.pattern.creationalPattern.singletonPattern;
public class Singleton {
//Q:這裏可不可以去掉static?
//A:不可以,因爲getInstance()方法是靜態方法,不可以在靜態方法裏調用非靜態變量
private static Singleton instance;
private Singleton(){
//構造器私有化,防止外部訪問,通過new Singleton()方式創建對象
}
/**
* synchronized 關鍵字可以保證方法或者代碼塊在運行時,同一時刻只有一 個方法可以進入到臨界區,同時它還可以保證共享變量的內存可見性
* */
public synchronized static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
這種方式具備很好的 lazy loading,能夠在多線程中很好的工作,但是,效率低,99% 情況下不需要同步。
優點:第一次調用才初始化,避免內存浪費。
缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。
getInstance() 的性能對應用程序不是很關鍵(該方法使用不太頻繁)。
synchronized 關鍵字JDK隨着版本升級一直在進行優化,其實效率已經很不錯了,但是編程是一門科學性學科,實事求是的說,加這個關鍵字,會影響效率。
- 餓漢式
public class HungrySingleton {
//private 私有化,外部不得調用,static 啓動時就分配該對象內存
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){} //私有化構造器,外部無法調用
//提供類靜態方法,直接調用,得到唯一實例。
public static HungrySingleton getInstance(){
return instance;
}
}
這種方式比較常用,但容易產生垃圾對象。
優點:沒有加鎖,執行效率會提高。
缺點:類加載時就初始化,浪費內存。
它基於 classloader 機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化 instance 顯然沒有達到 lazy loading 的效果。
浪費內存的原因,這個類在你的項目啓動時,就會去加載到JVM裏,如果你沒有去使用,就佔據了一小塊內存,如果項目中存在很多單例對象,會導致資源的浪費。
- 雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)
package com.design.pattern.creationalPattern.singletonPattern;
public class Singleton {
//這裏加入volatile使其在多線程操作時,保持可見性
private volatile static Singleton instance;
private Singleton(){
//構造器私有化,防止外部訪問,通過new Singleton()方式創建對象
}
public static Singleton getInstance(){
if (instance == null){ //第一次檢查instance是不是null
synchronized (Singleton.class){ //第二次加鎖再去檢查一遍是不是空,防止多線程併發導致線程上下文切換,生成多個實例對象。
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
這種方式採用雙鎖機制,也是懶加載方式實現,安全且在多線程情況下能保持高性能。
getInstance() 的性能對應用程序很關鍵。
- 登記式/靜態內部類
package com.design.pattern.creationalPattern.singletonPattern;
public class Singleton {
private Singleton(){
//構造器私有化,防止外部訪問,通過new Singleton()方式創建對象
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
}
這種方式能達到雙檢鎖方式一樣的功效,但實現更簡單,所以使用比較廣泛。對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用於靜態域的情況,雙檢鎖方式可在實例域需要延遲初始化時使用。
這種方式同樣利用了 classloader 機制來保證初始化 instance 時只有一個線程
,它跟第 3 種方式不同的是:第 3 種方式只要 Singleton 類被裝載了,那麼 instance 就會被實例化(沒有達到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不一定被初始化
。因爲 SingletonHolder 類沒有被主動使用,只有通過顯式調用 getInstance 方法時,纔會顯式裝載 SingletonHolder 類,從而實例化 instance
。
想象一下,如果實例化 instance 很消耗資源,所以想讓它延遲加載,另外一方面,又不希望在 Singleton 類加載時就實例化,因爲不能確保 Singleton 類還可能在其他的地方被主動使用從而被加載,那麼這個時候實例化 instance 顯然是不合適的。這個時候,這種方式相比第 3 種方式就顯得很合理。
- 枚舉
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
這種實現方式還沒有被廣泛採用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止多次實例化。
這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化
。不過,由於 JDK1.5 之後才加入 enum 特性,因爲枚舉類型是線程安全的,並且只會裝載一次
,用這種方式寫不免讓人感覺生疏,在實際工作中,也很少用。
不能通過 reflection attack 來調用私有構造方法。