作為對象的創建模式[GOF95], 單例模式確保某一個類只有一個執行個體,而且自行執行個體化並向整個系統提供這個執行個體。這個類稱為單例類。
註:本文乃閻宏博士的《Java與模式》一書的第十五章。
引言
單例模式的要點
單例單例
顯然單例模式的要點有三個;一是某各類只能有一個執行個體;二是它必須自行建立這個案例;三是它必須自行向整個系統提供這個執行個體。在下面的對象圖中,有一個 "單例對象",而"客戶甲"、"客戶乙" 和"客戶丙"是單例對象的三個客戶對象。可以看到,所有的客戶對象共用一個單例對象。而且從單例對象到自身的連接線可以看出,單例對象持有對自己的引用。
資源管理
一些資源管理員常常設計成單例模式。
在電腦系統中,需要管理的資源套件括軟體外部資源,譬如每台電腦可以有若干個印表機,但只能有一個Printer Spooler, 以避免兩個列印工作同時輸出到印表機中。每台電腦可以有若干傳真卡,但是只應該有一個軟體負責管理傳真卡,以避免出現兩份傳真作業同時傳到傳真卡中的情況。每台電腦可以有若干通訊連接埠,系統應當集中管理這些通訊連接埠,以避免一個通訊連接埠同時被兩個請求同時調用。
需要管理的資源套件括軟體內部資源,譬如,大多數的軟體都有一個(甚至多個)屬性(properties)檔案存放系統配置。這樣的系統應當由一個對象來管理一個屬性檔案。
需要管理的軟體內部資源也包括譬如負責記錄網站來訪人數的組件,記錄軟體系統內部事件、出錯資訊的組件,或是對系統的表現進行檢查的組件等。這些組件都必須集中管理,不可政出多頭。
這些資源管理員構件必須只有一個執行個體,這是其一;它們必須自行初始化,這是其二;允許整個系統訪問自己這是其三。因此,它們都滿足單例模式的條件,是單例模式的應用。
一個例子:Windows 資源回收筒
Windows 9x 以後的視窗系統中都有一個資源回收筒,就顯示了Windows 2000 的資源回收筒。
在整個視窗系統中,資源回收筒只能有一個執行個體,整個系統都使用這個惟一的執行個體,而且資源回收筒自行提供自己的執行個體。因此,資源回收筒是單例模式的應用。
雙重檢查成例
在本章最後的附錄裡研究了雙重檢查成例。雙重檢查成例與單例模式並無直接的關係,但是由於很多C 語言設計師在單例模式裡面使用雙重檢查成例,所以這一做法也被很多Java 設計師所模仿。因此,本書在附錄裡提醒讀者,雙重檢查成例在Java 語言裡並不能成立,詳情請見本章的附錄。
單例模式的結構
單例模式有以下的特點:
.. 單例類只可有一個執行個體。
.. 單例類必須自己建立自己這惟一的執行個體。
.. 單例類必須給所有其他對象提供這一執行個體。
雖然單例模式中的單例類被限定只能有一個執行個體,但是單例模式和單例類可以很容易被推廣到任意且有限多個執行個體的情況,這時候稱它為多例模式 (Multiton Pattern) 和多例類(Multiton Class),請見"專題:多例(Multiton )模式與多語言支援"一章。單例類的簡略類圖如下所示。
由於Java 語言的特點,使得單例模式在Java 語言的實現上有自己的特點。這些特點主要表現在單例類如何將自己執行個體化上。
餓漢式單例類餓漢式單例類是在Java 語言裡實現得最為簡便的單例類,下面所示的類圖描述了一個餓漢式單例類的典型實現。
可以看出,此類已經自已將自己執行個體化。
代碼清單1:餓漢式單例類
public class EagerSingleton { private static final EagerSingleton m_instance = new EagerSingleton(); /** * 私人的預設構造子 */ private EagerSingleton() { } /** * 靜態Factory 方法 */ public static EagerSingleton getInstance() {·224·Java 與模式 return m_instance; } } |
讀者可以看出,在這個類被載入時,靜態變數m_instance 會被初始化,此時類的私人構造子會被調用。這時候,單例類的惟一執行個體就被建立出來了。
Java 語言中單例類的一個最重要的特點是類的構造子是私人的,從而避免外界利用構造子直接建立出任意多的執行個體。值得指出的是,由於構造子是私人的,因此,此類不能被繼承。
懶漢式單例類
與餓漢式單例類相同之處是,類的構造子是私人的。與餓漢式單例類不同的是,懶漢式單例類在第一次被引用時將自己執行個體化。如果載入器是靜態,那麼在懶漢式單例類被載入時不會將自己執行個體化。如所示,類圖中給出了一個典型的懶漢式單例類實現。
代碼清單2:懶漢式單例類
package com.javapatterns.singleton.demos; public class LazySingleton { private static LazySingleton m_instance = null; /** * 私人的預設構造子,保證外界無法直接執行個體化 */ private LazySingleton() { } /** * 靜態Factory 方法,返還此類的惟一執行個體 */ synchronized public static LazySingleton getInstance() { if (m_instance == null) { m_instance = new LazySingleton(); } return m_instance; } } |
讀者可能會注意到,在上面給出懶漢式單例類實現裡對靜態Factory 方法使用了同步化,以處理多線程環境。有些設計師在這裡建議使用所謂的"雙重檢查成例"。必須指出的是,"雙重檢查成例"不可以在Java 語言中使用。不十分熟悉的讀者,可以看看後面給出的小節。
同樣,由於構造子是私人的,因此,此類不能被繼承。餓漢式單例類在自己被載入時就將自己執行個體化。即便載入器是靜態,在餓漢式單例類被載入時仍會將自己執行個體化。單從資源利用效率角度來講,這個比懶漢式單例類稍差些。
從速度和反應時間角度來講,則比懶漢式單例類稍好些。然而,懶漢式單例類在執行個體化時, 必須處理好在多個線程同時首次引用此類時的訪問限制問題,特別是當單例類作為資源控制器,在執行個體化時必然涉及資源初始化,而資源初始化很有可能耗費時間。 這意味著出現多線程同時首次引用此類的機率變得較大。
餓漢式單例類可以在Java 語言內實現, 但不易在C++ 內實現,因為靜態初始化在C++ 裡沒有固定的順序,因而靜態m_instance 變數的初始化與類的載入順序沒有保證,可能會出問題。這就是為什麼GoF 在提出單例類的概念時,舉的例子是懶漢式的。他們的書影響之大,以致Java 語言中單例類的例子也大多是懶漢式的。實際上,本書認為餓漢式單例類更符合Java 語言本身的特點。
登記式單例類
登記式單例類是GoF 為了克服餓漢式單例類及懶漢式單例類均不可繼承的缺點而設計的。本書把他們的例子翻譯為Java 語言,並將它自己執行個體化的方式從懶漢式改為餓漢式。只是它的子類執行個體化的方式只能是懶漢式的, 這是無法改變的。如所示是登記式單例類的一個例子,圖中的關係線表明,此類已將自己執行個體化。
代碼清單3:登記式單例類
import java.util.HashMap; public class RegSingleton { static private HashMap m_registry = new HashMap(); static { RegSingleton x = new RegSingleton(); m_registry.put( x.getClass().getName() , x); } /** * 保護的預設構造子 */ protected RegSingleton() {} /** * 靜態Factory 方法,返還此類惟一的執行個體 */ static public RegSingleton getInstance(String name) { if (name == null) { name = "com.javapatterns.singleton.demos.RegSingleton"; } if (m_registry.get(name) == null) { try { m_registry.put( name, Class.forName(name).newInstance() ) ; } catch(Exception e) { System.out.println("Error happened."); } } return (RegSingleton) (m_registry.get(name) ); } /** * 一個示意性的商業方法 */ public String about() { return "Hello, I am RegSingleton."; } } |
它的子類RegSingletonChild 需要父類的協助才能執行個體化。所示是登記式單例類子類的一個例子。圖中的關係表明,此類是由父類將子類執行個體化的。
下面是子類的原始碼。
代碼清單4:登記式單例類的子類
import java.util.HashMap; public class RegSingletonChild extends RegSingleton { public RegSingletonChild() {} /** * 靜態Factory 方法 */ static public RegSingletonChild getInstance() { return (RegSingletonChild) RegSingleton.getInstance( "com.javapatterns.singleton.demos.RegSingletonChild" ); } /** * 一個示意性的商業方法 */ public String about() { return "Hello, I am RegSingletonChild."; } } |
在GoF 原始的例子中,並沒有getInstance() 方法,這樣得到子類必須調用的getInstance(String name)方法並傳入子類的名字,因此很不方便。本章在登記式單例類子類的例子裡,加入了getInstance() 方法,這樣做的好處是RegSingletonChild 可以通過這個方法,返還自已的執行個體。而這樣做的缺點是,由於資料類型不同,無法在RegSingleton 提供這樣一個方法。由於子類必須允許父類以構造子調用產生執行個體,因此,它的構造子必須是公開的。這樣一來,就等於允許了以這樣方式產生執行個體而不在父類的登 記中。這是登記式單例類的一個缺點。
GoF 曾指出,由於父類的執行個體必須存在才可能有子類的執行個體,這在有些情況下是一個浪費。這是登記式單例類的另一個缺點。