測試單例模式
接下來,我使用與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()方法由于下面的代碼而不是線程安全的:
- 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()方法:
- 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()方法的實現.然而,我們必須要研究另一個問題:你必須在編譯期指定這個單例類,這樣就不是很靈活.一個單例類的注冊表會讓我們在運行期指定一個單例類.