概要
單例模式是最簡單的設計模式之一,但是對于Java的開發者來說,它卻有很多缺陷。在本月的專欄中,David Geary探討了單例模式以及在面對多線程(multithreading)、類裝載器(classloaders)和序列化(serialization)時如何處理這些缺陷。
單例模式適合于一個類只有一個實例的情況,比如窗口管理器,打印緩沖池和文件系統,它們都是原型的例子。典型的情況是,那些對象的類型被遍及一個軟件系統的不同對象訪問,因此需要一個全局的訪問指針,這便是眾所周知的單例模式的應用。當然這只有在你確信你不再需要任何多于一個的實例的情況下。
單例模式的用意在于前一段中所關心的。通過單例模式你可以:
確保一個類只有一個實例被建立
提供了一個對對象的全局訪問指針
在不影響單例類的客戶端的情況下允許將來有多個實例
盡管單例設計模式如在下面的圖中的所顯示的一樣是最簡單的設計模式,但對于粗心的Java開發者來說卻呈現出許多缺陷。這篇文章討論了單例模式并揭示了那些缺陷。
注意:你可以從Resources下載這篇文章的源代碼。
單例模式
在《設計模式》一書中,作者這樣來敘述單例模式的:確保一個類只有一個實例并提供一個對它的全局訪問指針。
下圖說明了單例模式的類圖。
(圖1)
單例模式的類圖
正如你在上圖中所看到的,這不是單例模式的完整部分。此圖中單例類保持了一個對唯一的單例實例的靜態引用,并且會從靜態getInstance()方法中返回對那個實例的引用。
例1顯示了一個經典的單例模式的實現。
例1.經典的單例模式
- public class ClassicSingleton {
- private static ClassicSingleton instance = null;
- protected ClassicSingleton() {
- // Exists only to defeat instantiation.
- }
- public static ClassicSingleton getInstance() {
- if(instance == null) {
- instance = new ClassicSingleton();
- }
- return instance;
- }
- }
在例1中的單例模式的實現很容易理解。ClassicSingleton類保持了一個對單獨的單例實例的靜態引用,并且從靜態方法getInstance()中返回那個引用。
關于ClassicSingleton類,有幾個讓我們感興趣的地方。首先,ClassicSingleton使用了一個眾所周知的懶漢式實例化去創建那個單例類的引用;結果,這個單例類的實例直到getInstance()方法被第一次調用時才被創建。這種技巧可以確保單例類的實例只有在需要時才被建立出來。其次,注意ClassicSingleton實現了一個protected的構造方法,這樣客戶端不能直接實例化一個ClassicSingleton類的實例。然而,你會驚奇的發現下面的代碼完全合法:
- public class SingletonInstantiator {
- public SingletonInstantiator() {
- ClassicSingleton instance = ClassicSingleton.getInstance();
- ClassicSingleton anotherInstance =
- new ClassicSingleton();
- ...
- }
- }
前面這個代碼片段為何能在沒有繼承ClassicSingleton并且ClassicSingleton類的構造方法是protected的情況下創建其實例?答案是protected的構造方法可以被其子類以及在同一個包中的其它類調用。因為ClassicSingleton和SingletonInstantiator位于相同的包(缺省的包),所以SingletonInstantiator方法能創建ClasicSingleton的實例。
這種情況下有兩種解決方案:一是你可以使ClassicSingleton的構造方法變化私有的(private)這樣只有ClassicSingleton的方法能調用它;然而這也意味著ClassicSingleton不能有子類。有時這是一種很合意的解決方法,如果確實如此,那聲明你的單例類為final是一個好主意,這樣意圖明確,并且讓編譯器去使用一些性能優化選項。另一種解決方法是把你的單例類放到一個外在的包中,以便在其它包中的類(包括缺省的包)無法實例化一個單例類。
關于ClassicSingleton的第三點感興趣的地方是,如果單例由不同的類裝載器裝入,那便有可能存在多個單例類的實例。假定不是遠端存取,例如一些servlet容器對每個servlet使用完全不同的類裝載器,這樣的話如果有兩個servlet訪問一個單例類,它們就都會有各自的實例。
第四點,如果ClasicSingleton實現了java.io.Serializable接口,那么這個類的實例就可能被序列化和復原。不管怎樣,如果你序列化一個單例類的對象,接下來復原多個那個對象,那你就會有多個單例類的實例。
最后也許是最重要的一點,就是例1中的ClassicSingleton類不是線程安全的。如果兩個線程,我們稱它們為線程1和線程2,在同一時間調用ClassicSingleton.getInstance()方法,如果線程1先進入if塊,然后線程2進行控制,那么就會有ClassicSingleton的兩個的實例被創建。
正如你從前面的討論中所看到的,盡管單例模式是最簡單的設計模式之一,在Java中實現它也是決非想象的那么簡單。這篇文章接下來會揭示Java規范對單例模式進行的考慮,但是首先讓我們近水樓臺的看看你如何才能測試你的單例類。
測試單例模式
接下來,我使用與log4j相對應的JUnit來測試單例類,它會貫穿在這篇文章余下的部分。如果你對JUnit或log4j不很熟悉,請參考相關資源。
例2是一個用JUnit測試例1的單例模式的案例:
例2.一個單例模式的案例
- import org.apache.log4j.Logger;
- import junit.framework.Assert;
- import junit.framework.TestCase;
- public class SingletonTest extends TestCase {
- private ClassicSingleton sone = null, stwo = null;
- private static Logger logger = Logger.getRootLogger();
- public SingletonTest(String name) {
- super(name);
- }
- public void setUp() {
- logger.info("getting singleton...");
- sone = ClassicSingleton.getInstance();
- logger.info("...got singleton: " + sone);
- logger.info("getting singleton...");
- stwo = ClassicSingleton.getInstance();
- logger.info("...got singleton: " + stwo);
- }
- public void testUnique() {
- logger.info("checking singletons for equality");
- Assert.assertEquals(true, sone == stwo);
- }
- }
例2兩次調用ClassicSingleton.getInstance(),并且把返回的引用存儲在成員變量中。方法testUnique()會檢查這些引用看它們是否相同。例3是這個測試案例的輸出:
例3.是這個測試案例的輸出
- Buildfile: build.xml
- init:
- [echo] Build 20030414 (14-04-2003 03:08)
- compile:
- run-test-text:
- [java] .INFO main: <STRONG>getting singleton...</STRONG>
- [java] INFO main: <STRONG>created singleton:</STRONG> Singleton@e86f41
- [java] INFO main: ...got singleton: Singleton@e86f41
- [java] INFO main: <STRONG>getting singleton...</STRONG>
- [java] INFO main: ...got singleton: Singleton@e86f41
- [java] INFO main: checking singletons for equality
- [java] Time: 0.032
- [java] OK (1 test)
正如前面的清單所示,例2的簡單測試順利通過----通過ClassicSingleton.getInstance()獲得的兩個單例類的引用確實相同;然而,你要知道這些引用是在單線程中得到的。下面的部分著重于用多線程測試單例類。