作為對(duì)象的創(chuàng)建模式[GOF95], 單例模式確保某一個(gè)類(lèi)只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。這個(gè)類(lèi)稱(chēng)為單例類(lèi)。
注:本文乃閻宏博士的《Java與模式》一書(shū)的第十五章。
引言
單例模式的要點(diǎn)
單例單例
顯然單例模式的要點(diǎn)有三個(gè);一是某各類(lèi)只能有一個(gè)實(shí)例;二是它必須自行創(chuàng)建這個(gè)事例;三是它必須自行向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。在下面的對(duì)象圖中,有一個(gè)"單例對(duì)象",而"客戶(hù)甲"、"客戶(hù)乙" 和"客戶(hù)丙"是單例對(duì)象的三個(gè)客戶(hù)對(duì)象。可以看到,所有的客戶(hù)對(duì)象共享一個(gè)單例對(duì)象。而且從單例對(duì)象到自身的連接線(xiàn)可以看出,單例對(duì)象持有對(duì)自己的引用。

資源管理
一些資源管理器常常設(shè)計(jì)成單例模式。
在計(jì)算機(jī)系統(tǒng)中,需要管理的資源包括軟件外部資源,譬如每臺(tái)計(jì)算機(jī)可以有若干個(gè)打印機(jī),但只能有一個(gè)Printer Spooler, 以避免兩個(gè)打印作業(yè)同時(shí)輸出到打印機(jī)中。每臺(tái)計(jì)算機(jī)可以有若干傳真卡,但是只應(yīng)該有一個(gè)軟件負(fù)責(zé)管理傳真卡,以避免出現(xiàn)兩份傳真作業(yè)同時(shí)傳到傳真卡中的情況。每臺(tái)計(jì)算機(jī)可以有若干通信端口,系統(tǒng)應(yīng)當(dāng)集中管理這些通信端口,以避免一個(gè)通信端口同時(shí)被兩個(gè)請(qǐng)求同時(shí)調(diào)用。
需要管理的資源包括軟件內(nèi)部資源,譬如,大多數(shù)的軟件都有一個(gè)(甚至多個(gè))屬性(properties)文件存放系統(tǒng)配置。這樣的系統(tǒng)應(yīng)當(dāng)由一個(gè)對(duì)象來(lái)管理一個(gè)屬性文件。
需要管理的軟件內(nèi)部資源也包括譬如負(fù)責(zé)記錄網(wǎng)站來(lái)訪(fǎng)人數(shù)的部件,記錄軟件系統(tǒng)內(nèi)部事件、出錯(cuò)信息的部件,或是對(duì)系統(tǒng)的表現(xiàn)進(jìn)行檢查的部件等。這些部件都必須集中管理,不可政出多頭。
這些資源管理器構(gòu)件必須只有一個(gè)實(shí)例,這是其一;它們必須自行初始化,這是其二;允許整個(gè)系統(tǒng)訪(fǎng)問(wèn)自己這是其三。因此,它們都滿(mǎn)足單例模式的條件,是單例模式的應(yīng)用。
一個(gè)例子:Windows 回收站
Windows 9x 以后的視窗系統(tǒng)中都有一個(gè)回收站,下圖就顯示了Windows 2000 的回收站。

在整個(gè)視窗系統(tǒng)中,回收站只能有一個(gè)實(shí)例,整個(gè)系統(tǒng)都使用這個(gè)惟一的實(shí)例,而且回收站自行提供自己的實(shí)例。因此,回收站是單例模式的應(yīng)用。
雙重檢查成例
在本章最后的附錄里研究了雙重檢查成例。雙重檢查成例與單例模式并無(wú)直接的關(guān)系,但是由于很多C 語(yǔ)言設(shè)計(jì)師在單例模式里面使用雙重檢查成例,所以這一做法也被很多Java 設(shè)計(jì)師所模仿。因此,本書(shū)在附錄里提醒讀者,雙重檢查成例在Java 語(yǔ)言里并不能成立,詳情請(qǐng)見(jiàn)本章的附錄。
單例模式的結(jié)構(gòu)
單例模式有以下的特點(diǎn):
.. 單例類(lèi)只可有一個(gè)實(shí)例。
.. 單例類(lèi)必須自己創(chuàng)建自己這惟一的實(shí)例。
.. 單例類(lèi)必須給所有其他對(duì)象提供這一實(shí)例。
雖然單例模式中的單例類(lèi)被限定只能有一個(gè)實(shí)例,但是單例模式和單例類(lèi)可以很容易被推廣到任意且有限多個(gè)實(shí)例的情況,這時(shí)候稱(chēng)它為多例模式(Multiton Pattern) 和多例類(lèi)(Multiton Class),請(qǐng)見(jiàn)"專(zhuān)題:多例(Multiton )模式與多語(yǔ)言支持"一章。單例類(lèi)的簡(jiǎn)略類(lèi)圖如下所示。
由于Java 語(yǔ)言的特點(diǎn),使得單例模式在Java 語(yǔ)言的實(shí)現(xiàn)上有自己的特點(diǎn)。這些特點(diǎn)主要表現(xiàn)在單例類(lèi)如何將自己實(shí)例化上。
餓漢式單例類(lèi)餓漢式單例類(lèi)是在Java 語(yǔ)言里實(shí)現(xiàn)得最為簡(jiǎn)便的單例類(lèi),下面所示的類(lèi)圖描述了一個(gè)餓漢式單例類(lèi)的典型實(shí)現(xiàn)。
從圖中可以看出,此類(lèi)已經(jīng)自已將自己實(shí)例化。
代碼清單1:餓漢式單例類(lèi)
public class EagerSingleton { private static final EagerSingleton m_instance = new EagerSingleton(); /** * 私有的默認(rèn)構(gòu)造子 */ private EagerSingleton() { } /** * 靜態(tài)工廠(chǎng)方法 */ public static EagerSingleton getInstance() {
·224·Java 與模式 return m_instance; } }
讀者可以看出,在這個(gè)類(lèi)被加載時(shí),靜態(tài)變量m_instance 會(huì)被初始化,此時(shí)類(lèi)的私有構(gòu)造子會(huì)被調(diào)用。這時(shí)候,單例類(lèi)的惟一實(shí)例就被創(chuàng)建出來(lái)了。
Java 語(yǔ)言中單例類(lèi)的一個(gè)最重要的特點(diǎn)是類(lèi)的構(gòu)造子是私有的,從而避免外界利用構(gòu)造子直接創(chuàng)建出任意多的實(shí)例。值得指出的是,由于構(gòu)造子是私有的,因此,此類(lèi)不能被繼承。
懶漢式單例類(lèi)
與餓漢式單例類(lèi)相同之處是,類(lèi)的構(gòu)造子是私有的。與餓漢式單例類(lèi)不同的是,懶漢式單例類(lèi)在第一次被引用時(shí)將自己實(shí)例化。如果加載器是靜態(tài)的,那么在懶漢式單例類(lèi)被加載時(shí)不會(huì)將自己實(shí)例化。如下圖所示,類(lèi)圖中給出了一個(gè)典型的餓漢式單例類(lèi)實(shí)現(xiàn)。

代碼清單2:懶漢式單例類(lèi)
package com.javapatterns.singleton.demos; public class LazySingleton { private static LazySingleton m_instance = null; /** * 私有的默認(rèn)構(gòu)造子,保證外界無(wú)法直接實(shí)例化 */ private LazySingleton() { } /** * 靜態(tài)工廠(chǎng)方法,返還此類(lèi)的惟一實(shí)例 */ synchronized public static LazySingleton getInstance() { if (m_instance == null) { m_instance = new LazySingleton(); } return m_instance; } }
讀者可能會(huì)注意到,在上面給出懶漢式單例類(lèi)實(shí)現(xiàn)里對(duì)靜態(tài)工廠(chǎng)方法使用了同步化,以處理多線(xiàn)程環(huán)境。有些設(shè)計(jì)師在這里建議使用所謂的"雙重檢查成例"。必須指出的是,"雙重檢查成例"不可以在Java 語(yǔ)言中使用。不十分熟悉的讀者,可以看看后面給出的小節(jié)。
同樣,由于構(gòu)造子是私有的,因此,此類(lèi)不能被繼承。餓漢式單例類(lèi)在自己被加載時(shí)就將自己實(shí)例化。即便加載器是靜態(tài)的,在餓漢式單例類(lèi)被加載時(shí)仍會(huì)將自己實(shí)例化。單從資源利用效率角度來(lái)講,這個(gè)比懶漢式單例類(lèi)稍差些。
從速度和反應(yīng)時(shí)間角度來(lái)講,則比懶漢式單例類(lèi)稍好些。然而,懶漢式單例類(lèi)在實(shí)例化時(shí), 必須處理好在多個(gè)線(xiàn)程同時(shí)首次引用此類(lèi)時(shí)的訪(fǎng)問(wèn)限制問(wèn)題,特別是當(dāng)單例類(lèi)作為資源控制器,在實(shí)例化時(shí)必然涉及資源初始化,而資源初始化很有可能耗費(fèi)時(shí)間。這意味著出現(xiàn)多線(xiàn)程同時(shí)首次引用此類(lèi)的機(jī)率變得較大。
餓漢式單例類(lèi)可以在Java 語(yǔ)言?xún)?nèi)實(shí)現(xiàn), 但不易在C++ 內(nèi)實(shí)現(xiàn),因?yàn)殪o態(tài)初始化在C++ 里沒(méi)有固定的順序,因而靜態(tài)的m_instance 變量的初始化與類(lèi)的加載順序沒(méi)有保證,可能會(huì)出問(wèn)題。這就是為什么GoF 在提出單例類(lèi)的概念時(shí),舉的例子是懶漢式的。他們的書(shū)影響之大,以致Java 語(yǔ)言中單例類(lèi)的例子也大多是懶漢式的。實(shí)際上,本書(shū)認(rèn)為餓漢式單例類(lèi)更符合Java 語(yǔ)言本身的特點(diǎn)。
登記式單例類(lèi)
登記式單例類(lèi)是GoF 為了克服餓漢式單例類(lèi)及懶漢式單例類(lèi)均不可繼承的缺點(diǎn)而設(shè)計(jì)的。本書(shū)把他們的例子翻譯為Java 語(yǔ)言,并將它自己實(shí)例化的方式從懶漢式改為餓漢式。只是它的子類(lèi)實(shí)例化的方式只能是懶漢式的, 這是無(wú)法改變的。如下圖所示是登記式單例類(lèi)的一個(gè)例子,圖中的關(guān)系線(xiàn)表明,此類(lèi)已將自己實(shí)例化。

代碼清單3:登記式單例類(lèi)
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); } /** * 保護(hù)的默認(rèn)構(gòu)造子 */ protected RegSingleton() {} /** * 靜態(tài)工廠(chǎng)方法,返還此類(lèi)惟一的實(shí)例 */ 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) ); } /** * 一個(gè)示意性的商業(yè)方法 */ public String about() { return "Hello, I am RegSingleton."; } } 它的子類(lèi)RegSingletonChild 需要父類(lèi)的幫助才能實(shí)例化。下圖所示是登記式單例類(lèi)子類(lèi)的一個(gè)例子。圖中的關(guān)系表明,此類(lèi)是由父類(lèi)將子類(lèi)實(shí)例化的。

下面是子類(lèi)的源代碼。
代碼清單4:登記式單例類(lèi)的子類(lèi)
import java.util.HashMap; public class RegSingletonChild extends RegSingleton { public RegSingletonChild() {} /** * 靜態(tài)工廠(chǎng)方法 */ static public RegSingletonChild getInstance() { return (RegSingletonChild) RegSingleton.getInstance( "com.javapatterns.singleton.demos.RegSingletonChild" ); } /** * 一個(gè)示意性的商業(yè)方法 */ public String about() { return "Hello, I am RegSingletonChild."; } }
在GoF 原始的例子中,并沒(méi)有g(shù)etInstance() 方法,這樣得到子類(lèi)必須調(diào)用的getInstance(String name)方法并傳入子類(lèi)的名字,因此很不方便。本章在登記式單例類(lèi)子類(lèi)的例子里,加入了getInstance() 方法,這樣做的好處是RegSingletonChild 可以通過(guò)這個(gè)方法,返還自已的實(shí)例。而這樣做的缺點(diǎn)是,由于數(shù)據(jù)類(lèi)型不同,無(wú)法在RegSingleton 提供這樣一個(gè)方法。由于子類(lèi)必須允許父類(lèi)以構(gòu)造子調(diào)用產(chǎn)生實(shí)例,因此,它的構(gòu)造子必須是公開(kāi)的。這樣一來(lái),就等于允許了以這樣方式產(chǎn)生實(shí)例而不在父類(lèi)的登記中。這是登記式單例類(lèi)的一個(gè)缺點(diǎn)。
GoF 曾指出,由于父類(lèi)的實(shí)例必須存在才可能有子類(lèi)的實(shí)例,這在有些情況下是一個(gè)浪費(fèi)。這是登記式單例類(lèi)的另一個(gè)缺點(diǎn)。 |