Java模式設計之單例模式
作為對象的創建模式[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() { } /** * 靜態工廠方法 */ 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() { } /** * 靜態工廠方法,返還此類的惟一實例 */ synchronized public static LazySingleton getInstance() { if (m_instance == null) { m_instance = new LazySingleton(); } return m_instance; } } |
讀者可能會注意到,在上面給出懶漢式單例類實現里對靜態工廠方法使用了同步化,以處理多線程環境。有些設計師在這里建議使用所謂的"雙重檢查成例"。必須指出的是,"雙重檢查成例"不可以在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() {} /** * 靜態工廠方法,返還此類惟一的實例 */ 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() {} /** * 靜態工廠方法 */ 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 曾指出,由于父類的實例必須存在才可能有子類的實例,這在有些情況下是一個浪費。這是登記式單例類的另一個缺點。
posted on 2007-05-21 12:33 都市淘沙者 閱讀(133) 評論(0) 編輯 收藏 所屬分類: Java Basic/Lucene/開源資料