概要
單例模式是最簡單的設計模式之一,可是對於Java的開發者來講,它卻有不少缺陷。在本月的專欄中,David Geary探討了單例模式以及在面對多線程(multithreading)、類裝載器(classloaders)和序列化(serialization)時如何處理這些缺陷。
單例模式適合於一個類只有一個實例的狀況,好比窗口管理器,打印緩衝池和文件系統,它們都是原型的例子。典型的狀況是,那些對象的類型被遍佈一個軟件系統的不一樣對象訪問,所以須要一個全局的訪問指針,這即是衆所周知的單例模式的應用。固然這隻有在你確信你再也不須要任何多於一個的實例的狀況下。
單例模式的用意在於前一段中所關心的。經過單例模式你能夠:
確保一個類只有一個實例被創建
提供了一個對對象的全局訪問指針
在不影響單例類的客戶端的狀況下容許未來有多個實例
儘管單例設計模式如在下面的圖中的所顯示的同樣是最簡單的設計模式,但對於粗心的Java開發者來講卻呈現出許多缺陷。這篇文章討論了單例模式並揭示了那些缺陷。
注意:你能夠從Resources下載這篇文章的源代碼。
單例模式
在《設計模式》一書中,做者這樣來敘述單例模式的:確保一個類只有一個實例並提供一個對它的全局訪問指針。
下圖說明了單例模式的類圖。
(圖1)
單例模式的類圖
正如你在上圖中所看到的,這不是單例模式的完整部分。此圖中單例類保持了一個對惟一的單例實例的靜態引用,而且會從靜態getInstance()方法中返回對那個實例的引用。
例1顯示了一個經典的單例模式的實現。
例1.經典的單例模式 html
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: [b]getting singleton...[/b]
[java] INFO main: [b]created singleton:[/b] Singleton@e86f41
[java] INFO main: ...got singleton: Singleton@e86f41
[java] INFO main: [b]getting singleton...[/b]
[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()得到的兩個單例類的引用確實相同;然而,你要知道這些引用是在單線程中獲得的。下面的部分着重於用多線程測試單例類。
多線程因素的考慮
在例1中的ClassicSingleton.getInstance()方法因爲下面的代碼而不是線程安全的:java
1: if(instance == null) {
2: instance = new Singleton();
3: }
若是一個線程在第二行的賦值語句發生以前切換,那麼成員變量instance仍然是null,而後另外一個線程可能接下來進入到if塊中。在這種狀況下,兩個不一樣的單例類實例就被建立。不幸的是這種假定不多發生,這樣這種假定也很難在測試期間出現(譯註:在這多是做者對不多出現這種狀況而致使沒法測試從而令人們放鬆警戒而感到嘆惜)。爲了演示這個線程輪換,我得從新實現例1中的那個類。例4就是修訂後的單例類:
例4.人爲安排的方式
import org.apache.log4j.Logger;
public class Singleton {
private static Singleton singleton = null;
private static Logger logger = Logger.getRootLogger();
private static boolean firstThread = true;
protected Singleton() {
// Exists only to defeat instantiation.
}
public static Singleton getInstance() {
if(singleton == null) {
simulateRandomActivity();
singleton = new Singleton();
}
logger.info("created singleton: " + singleton);
return singleton;
}
private static void simulateRandomActivity() {
try {
if(firstThread) {
firstThread = false;
logger.info("sleeping...");
// This nap should give the second thread enough time
// to get by the first thread.
Thread.currentThread().sleep(50);
}
}
catch(InterruptedException ex) {
logger.warn("Sleep interrupted");
}
}
}
除了在這個清單中的單例類強制使用了一個多線程錯誤處理,例4相似於例1中的單例類。在getInstance()方法第一次被調用時,調用這個方法的線程會休眠50毫秒以便另外的線程也有時間調用getInstance()並建立一個新的單例類實例。當休眠的線程覺醒時,它也會建立一個新的單例類實例,這樣咱們就有兩個單例類實例。儘管例4是人爲如此的,但它卻模擬了第一個線程調用了getInstance()並在沒有完成時被切換的真實情形。
例5測試了例4的單例類:
例5.失敗的測試
import org.apache.log4j.Logger;
import junit.framework.Assert;
import junit.framework.TestCase;
public class SingletonTest extends TestCase {
private static Logger logger = Logger.getRootLogger();
private static Singleton singleton = null;
public SingletonTest(String name) {
super(name);
}
public void setUp() {
singleton = null;
}
public void testUnique() throws InterruptedException {
// Both threads call Singleton.getInstance().
Thread threadOne = new Thread(new SingletonTestRunnable()),
threadTwo = new Thread(new SingletonTestRunnable());
threadOne.start();
threadTwo.start();
threadOne.join();
threadTwo.join();
}
private static class SingletonTestRunnable implements Runnable {
public void run() {
// Get a reference to the singleton.
Singleton s = Singleton.getInstance();
// Protect singleton member variable from
// multithreaded access.
synchronized(SingletonTest.class) {
if(singleton == null) // If local reference is null...
singleton = s; // ...set it to the singleton
}
// Local reference must be equal to the one and
// only instance of Singleton; otherwise, we have two
// Singleton instances.
Assert.assertEquals(true, s == singleton);
}
}
}
例5的測試案例建立兩個線程,而後各自啓動,等待完成。這個案例保持了一個對單例類的靜態引用,每一個線程都會調用Singleton.getInstance()。若是這個靜態成員變量沒有被設置,那麼第一個線程就會將它設爲經過調用getInstance()而獲得的引用,而後這個靜態變量會與一個局部變量比較是否相等。
在這個測試案例運行時會發生一系列的事情:第一個線程調用getInstance(),進入if塊,而後休眠;接着,第二個線程也調用getInstance()而且建立了一個單例類的實例。第二個線程會設置這個靜態成員變量爲它所建立的引用。第二個線程檢查這個靜態成員變量與一個局部備份的相等性。而後測試經過。當第一個線程覺醒時,它也會建立一個單例類的實例,而且它不會設置那個靜態成員變量(由於第二個線程已經設置過了),因此那個靜態變量與那個局部變量脫離同步,相等性測試即告失敗。例6列出了例5的輸出:
例6.例5的輸出
- Buildfile: build.xml
- init:
- [echo] Build 20030414 (14-04-2003 03:06)
- compile:
- run-test-text:
- INFO Thread-1: sleeping...
- INFO Thread-2: created singleton: Singleton@7e5cbd
- INFO Thread-1: created singleton: Singleton@704ebb
- junit.framework.AssertionFailedError: expected: but was:
- at junit.framework.Assert.fail(Assert.java:47)
- at junit.framework.Assert.failNotEquals(Assert.java:282)
- at junit.framework.Assert.assertEquals(Assert.java:64)
- at junit.framework.Assert.assertEquals(Assert.java:149)
- at junit.framework.Assert.assertEquals(Assert.java:155)
- at SingletonTest$SingletonTestRunnable.run(Unknown Source)
- at java.lang.Thread.run(Thread.java:554)
- [java] .
- [java] Time: 0.577
-
- [java] OK (1 test)
到如今爲止咱們已經知道例4不是線程安全的,那就讓咱們看看如何修正它。
同步
要使例4的單例類爲線程安全的很容易----只要像下面一個同步化getInstance()方法:apache
public synchronized static Singleton getInstance() {
if(singleton == null) {
simulateRandomActivity();
singleton = new Singleton();
}
logger.info("created singleton: " + singleton);
return singleton;
}
在同步化getInstance()方法後,咱們就能夠獲得例5的測試案例返回的下面的結果:
- Buildfile: build.xml
-
- init:
- [echo] Build 20030414 (14-04-2003 03:15)
-
- compile:
- [javac] Compiling 2 source files
-
- run-test-text:
- INFO Thread-1: sleeping...
- INFO Thread-1: created singleton: Singleton@ef577d
- INFO Thread-2: created singleton: Singleton@ef577d
- [java] .
- [java] Time: 0.513
-
- [java] OK (1 test)
這此,這個測試案例工做正常,而且多線程的煩惱也被解決;然而,機敏的讀者可能會認識到getInstance()方法只須要在第一次被調用時同步。由於同步的性能開銷很昂貴(同步方法比非同步方法能下降到100次左右),或許咱們能夠引入一種性能改進方法,它只同步單例類的getInstance()方法中的賦值語句。
一種性能改進的方法
尋找一種性能改進方法時,你可能會選擇像下面這樣重寫getInstance()方法:設計模式
public static Singleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
這個代碼片斷只同步了關鍵的代碼,而不是同步整個方法。然而這段代碼卻不是線程安全的。考慮一下下面的假定:線程1進入同步塊,而且在它給singleton成員變量賦值以前線程1被切換。接着另外一個線程進入if塊。第二個線程將等待直到第一個線程完成,而且仍然會獲得兩個不一樣的單例類實例。有修復這個問題的方法嗎?請讀下去。
雙重加鎖檢查
初看上去,雙重加鎖檢查彷佛是一種使懶漢式實例化爲線程安全的技術。下面的代碼片斷展現了這種技術:安全
public static Singleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
若是兩個線程同時訪問getInstance()方法會發生什麼?想像一下線程1進行同步塊立刻又被切換。接着,第二個線程進入if 塊。當線程1退出同步塊時,線程2會從新檢查看是否singleton實例仍然爲null。由於線程1設置了singleton成員變量,因此線程2的第二次檢查會失敗,第二個單例類實例也就不會被建立。彷佛就是如此。
不幸的是,雙重加鎖檢查不會保證正常工做,由於編譯器會在Singleton的構造方法被調用以前隨意給singleton賦一個值。若是在singleton引用被賦值以後而被初始化以前線程1被切換,線程2就會被返回一個對未初始化的單例類實例的引用。
一個改進的線程安全的單例模式實現
例7列出了一個簡單、快速而又是線程安全的單例模式實現:
例7.一個簡單的單例類
public class Singleton {
public final static Singleton INSTANCE = new Singleton();
private Singleton() {
// Exists only to defeat instantiation.
}
}
這段代碼是線程安全的是由於靜態成員變量必定會在類被第一次訪問時被建立。你獲得了一個自動使用了懶漢式實例化的線程安全的實現;你應該這樣使用它:
Singleton singleton = Singleton.INSTANCE;
singleton.dothis();
singleton.dothat();
...
固然萬事並不完美,前面的Singleton只是一個折衷的方案;若是你使用那個實現,你就沒法改變它以便後來你可能想要容許多個單例類的實例。用一種更折哀的單例模式實現(經過一個getInstance()方法得到實例)你能夠改變這個方法以便返回一個惟一的實例或者是數百個實例中的一個.你不能用一個公開且是靜態的(public static)成員變量這樣作.
你能夠安全的使用例7的單例模式實現或者是例1的帶一個同步的getInstance()方法的實現.然而,咱們必需要研究另外一個問題:你必須在編譯期指定這個單例類,這樣就不是很靈活.一個單例類的註冊表會讓咱們在運行期指定一個單例類.
使用註冊表
使用一個單例類註冊表能夠:
在運行期指定單例類
防止產生多個單例類子類的實例
在例8的單例類中,保持了一個經過類名進行註冊的單例類註冊表:
例8 帶註冊表的單例類
import java.util.HashMap;
import org.apache.log4j.Logger;
public class Singleton {
private static HashMap map = new HashMap();
private static Logger logger = Logger.getRootLogger();
protected Singleton() {
// Exists only to thwart instantiation
}
public static synchronized Singleton getInstance(String classname) {
if(classname == null) throw new IllegalArgumentException("Illegal classname");
Singleton singleton = (Singleton)map.get(classname);
if(singleton != null) {
logger.info("got singleton from map: " + singleton);
return singleton;
}
if(classname.equals("SingeltonSubclass_One"))
singleton = new SingletonSubclass_One();
else if(classname.equals("SingeltonSubclass_Two"))
singleton = new SingletonSubclass_Two();
map.put(classname, singleton);
logger.info("created singleton: " + singleton);
return singleton;
}
// Assume functionality follows that's attractive to inherit
}
這段代碼的基類首先建立出子類的實例,而後把它們存儲在一個Map中。可是基類卻得付出很高的代價由於你必須爲每個子類替換它的getInstance()方法。幸運的是咱們可使用反射處理這個問題。
使用反射
在例9的帶註冊表的單例類中,使用反射來實例化一個特殊的類的對象。與例8相對的是經過這種實現,Singleton.getInstance()方法不須要在每一個被實現的子類中重寫了。
例9 使用反射實例化單例類
import java.util.HashMap;
import org.apache.log4j.Logger;
public class Singleton {
private static HashMap map = new HashMap();
private static Logger logger = Logger.getRootLogger();
protected Singleton() {
// Exists only to thwart instantiation
}
public static synchronized Singleton getInstance(String classname) {
Singleton singleton = (Singleton)map.get(classname);
if(singleton != null) {
logger.info("got singleton from map: " + singleton);
return singleton;
}
try {
singleton = (Singleton)Class.forName(classname).newInstance();
}
catch(ClassNotFoundException cnf) {
logger.fatal("Couldn't find class " + classname);
}
catch(InstantiationException ie) {
logger.fatal("Couldn't instantiate an object of type " + classname);
}
catch(IllegalAccessException ia) {
logger.fatal("Couldn't access class " + classname);
}
map.put(classname, singleton);
logger.info("created singleton: " + singleton);
return singleton;
}
}
關於單例類的註冊表應該說明的是:它們應該被封裝在它們本身的類中以便最大限度的進行復用。
封裝註冊表
例10列出了一個單例註冊表類。
例10 :一個SingletonRegistry類
import java.util.HashMap;
import org.apache.log4j.Logger;
public class SingletonRegistry {
public static SingletonRegistry REGISTRY = new SingletonRegistry();
private static HashMap map = new HashMap();
private static Logger logger = Logger.getRootLogger();
protected SingletonRegistry() {
// Exists to defeat instantiation
}
public static synchronized Object getInstance(String classname) {
Object singleton = map.get(classname);
if(singleton != null) {
return singleton;
}
try {
singleton = Class.forName(classname).newInstance();
logger.info("created singleton: " + singleton);
}
catch(ClassNotFoundException cnf) {
logger.fatal("Couldn't find class " + classname);
}
catch(InstantiationException ie) {
logger.fatal("Couldn't instantiate an object of type " +
classname);
}
catch(IllegalAccessException ia) {
logger.fatal("Couldn't access class " + classname);
}
map.put(classname, singleton);
return singleton;
}
}
注意我是把SingletonRegistry類做爲一個單例模式實現的。我也通用化了這個註冊表以便它能存儲和取回任何類型的對象。例11顯示了的Singleton類使用了這個註冊表。
例11 使用了一個封裝的註冊表的Singleton類
import java.util.HashMap;
import org.apache.log4j.Logger;
public class Singleton {
protected Singleton() {
// Exists only to thwart instantiation.
}
public static Singleton getInstance() {
return (Singleton)SingletonRegistry.REGISTRY.getInstance(classname);
}
}
上面的Singleton類使用那個註冊表的惟一實例經過類名取得單例對象。
如今咱們已經知道如何實現線程安全的單例類和如何使用一個註冊表去在運行期指定單例類名,接着讓咱們考查一下如何安排類載入器和處理序列化。
Classloaders
在許多狀況下,使用多個類載入器是很普通的--包括servlet容器--因此無論你在實現你的單例類時是多麼當心你都最終能夠獲得多個單例類的實例。若是你想要確保你的單例類只被同一個的類載入器裝入,那你就必須本身指定這個類載入器;例如:
private static Class getClass(String classname)
throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}
這個方法會嘗試把當前的線程與那個類載入器相關聯;若是classloader爲null,這個方法會使用與裝入單例類基類的那個類載入器。這個方法能夠用Class.forName()代替。
序列化
若是你序列化一個單例類,而後兩次重構它,那麼你就會獲得那個單例類的兩個實例,除非你實現readResolve()方法,像下面這樣:
例12 一個可序列化的單例類
import org.apache.log4j.Logger;
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
// Exists only to thwart instantiation.
}
private Object readResolve() {
return INSTANCE;
}
}
上面的單例類實現從readResolve()方法中返回一個惟一的實例;這樣不管Singleton類什麼時候被重構,它都只會返回那個相同的單例類實例。
例13測試了例12的單例類:
例13 測試一個可序列化的單例類
import java.io.*;
import org.apache.log4j.Logger;
import junit.framework.Assert;
import junit.framework.TestCase;
public class SingletonTest extends TestCase {
private Singleton sone = null, stwo = null;
private static Logger logger = Logger.getRootLogger();
public SingletonTest(String name) {
super(name);
}
public void setUp() {
sone = Singleton.INSTANCE;
stwo = Singleton.INSTANCE;
}
public void testSerialize() {
logger.info("testing singleton serialization...");
[b] writeSingleton();
Singleton s1 = readSingleton();
Singleton s2 = readSingleton();
Assert.assertEquals(true, s1 == s2);[/b] }
private void writeSingleton() {
try {
FileOutputStream fos = new FileOutputStream("serializedSingleton");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Singleton s = Singleton.INSTANCE;
oos.writeObject(Singleton.INSTANCE);
oos.flush();
}
catch(NotSerializableException se) {
logger.fatal("Not Serializable Exception: " + se.getMessage());
}
catch(IOException iox) {
logger.fatal("IO Exception: " + iox.getMessage());
}
}
private Singleton readSingleton() {
Singleton s = null;
try {
FileInputStream fis = new FileInputStream("serializedSingleton");
ObjectInputStream ois = new ObjectInputStream(fis);
s = (Singleton)ois.readObject();
}
catch(ClassNotFoundException cnf) {
logger.fatal("Class Not Found Exception: " + cnf.getMessage());
}
catch(NotSerializableException se) {
logger.fatal("Not Serializable Exception: " + se.getMessage());
}
catch(IOException iox) {
logger.fatal("IO Exception: " + iox.getMessage());
}
return s;
}
public void testUnique() {
logger.info("testing singleton uniqueness...");
Singleton another = new Singleton();
logger.info("checking singletons for equality");
Assert.assertEquals(true, sone == stwo);
}
}
前面這個測試案例序列化例12中的單例類,而且兩次重構它。而後這個測試案例檢查看是否被重構的單例類實例是同一個對象。下面是測試案例的輸出:
Buildfile: build.xml
init:
[echo] Build 20030422 (22-04-2003 11:32)
compile:
run-test-text:
[java] .INFO main: testing singleton serialization...
[java] .INFO main: testing singleton uniqueness...
[java] INFO main: checking singletons for equality
[java] Time: 0.1
[java] OK (2 tests)
單例模式結束語 單例模式簡單卻容易讓人迷惑,特別是對於Java的開發者來講。在這篇文章中,做者演示了Java開發者在顧及多線程、類載入器和序列化狀況如何實現單例模式。做者也展現了你怎樣才能實現一個單例類的註冊表,以便可以在運行期指定單例類