單例模式完全剖析_第1頁
單例模式完全剖析_第2頁
單例模式完全剖析_第3頁
單例模式完全剖析_第4頁
單例模式完全剖析_第5頁
已閱讀5頁,還剩14頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

1、單例模式完全剖析概要 單例模式是最簡單的設計模式之一,但是對于Java的開發者來說,它卻有很多缺陷。在本月的專欄中,David Geary探討了單例模式以及在面對多線程(multithreading)、類裝載器(classloaders)和序列化(serialization)時如何處理這些缺陷。 單例模式適合于一個類只有一個實例的情況,比如窗口管理器,打印緩沖池和文件系統,它們都是原型的例子。典型的情況是,那些對象的類型被遍及一個軟件系統的不同對象訪問,因此需要一個全局的訪問指針,這便是眾所周知的單例模式的應用。當然這只有在你確信你不再需要任何多于一個的實例的情況下。 單例模式的用意在于前一段

2、中所關心的。通過單例模式你可以: 確保一個類只有一個實例被建立 提供了一個對對象的全局訪問指針 在不影響單例類的客戶端的情況下允許將來有多個實例 盡管單例設計模式如在下面的圖中的所顯示的一樣是最簡單的設計模式,但對于粗心的Java開發者來說卻呈現出許多缺陷。這篇文章討論了單例模式并揭示了那些缺陷。 注意:你可以從Resources下載這篇文章的源代碼。 單例模式 在設計模式一書中,作者這樣來敘述單例模式的:確保一個類只有一個實例并提供一個對它的全局訪問指針。 下圖說明了單例模式的類圖。 (圖1) 單例模式的類圖 正如你在上圖中所看到的,這不是單例模式的完整部分。此圖中單例類保持了一個對唯一的單

3、例實例的靜態引用,并且會從靜態getInstance()方法中返回對那個實例的引用。 例1顯示了一個經典的單例模式的實現。 例1.經典的單例模式 Java代碼 1. public class ClassicSingleton     2.    private static ClassicSingleton instance = null;    3.      4.  

4、60; protected ClassicSingleton()     5.       / Exists only to defeat instantiation.    6.        7.    public static ClassicSingleton getIn

5、stance()     8.       if(instance = null)     9.          instance = new ClassicSingleton();    10.         &#

6、160; 11.       return instance;    12.        13.    public class ClassicSingleton private static ClassicSingleton instance = null; protected ClassicSingleton() / Exists only to defeat instantiation.

7、public static ClassicSingleton getInstance() if(instance = null) instance = new ClassicSingleton(); return instance; 在例1中的單例模式的實現很容易理解。ClassicSingleton類保持了一個對單獨的單例實例的靜態引用,并且從靜態方法getInstance()中返回那個引用。 關于ClassicSingleton類,有幾個讓我們感興趣的地方。首先,ClassicSingleton使用了一個眾所周知的懶漢式實例化去創建那個單例類的引用;結果,這個單例類的實例直到getInst

8、ance()方法被第一次調用時才被創建。這種技巧可以確保單例類的實例只有在需要時才被建立出來。其次,注意ClassicSingleton實現了一個protected的構造方法,這樣客戶端不能直接實例化一個ClassicSingleton類的實例。然而,你會驚奇的發現下面的代碼完全合法: Java代碼 1. public class SingletonInstantiator      2.   public SingletonInstantiator()    &#

9、160; 3.    ClassicSingleton instance = ClassicSingleton.getInstance();    4. ClassicSingleton anotherInstance =    5. new ClassicSingleton();    6.        .   &

10、#160; 7.        8.    public class SingletonInstantiator public SingletonInstantiator() ClassicSingleton instance = ClassicSingleton.getInstance(); ClassicSingleton anotherInstance = new ClassicSingleton(); . 測試單例模式 接下來,我使用與log4j相對應的JUnit來測試單例類,它會貫穿在

11、這篇文章余下的部分。如果你對JUnit或log4j不很熟悉,請參考相關資源。 例2是一個用JUnit測試例1的單例模式的案例: 例2.一個單例模式的案例 Java代碼 .      5. public class SingletonTest extends TestCase     6.    private ClassicSingleton sone = null, stwo

12、60;= null;    7.    private static Logger logger = Logger.getRootLogger();    8.      9.    public SingletonTest(String name)     10.     &#

13、160; super(name);    11.        12.    public void setUp()     13.       ("getting singleton.");    14.      

14、 sone = ClassicSingleton.getInstance();    15.       (".got singleton: " + sone);    16.      17.       ("getting 

15、;singleton.");    18.       stwo = ClassicSingleton.getInstance();    19.       (".got singleton: " + stwo);    20.     

16、;   21.    public void testUnique()     22.       ("checking singletons for equality");    23.       Assert.assertEquals(true, so

17、ne = stwo);    24.        25.     public class SingletonTest extends TestCase private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) super(name);

18、 public void setUp() ("getting singleton."); sone = ClassicSingleton.getInstance(); (".got singleton: " + sone); ("getting singleton."); stwo = ClassicSingleton.getInstance(); (".got singleton: " + stwo); public void

19、 testUnique() ("checking singletons for equality"); Assert.assertEquals(true, sone = stwo); 例2兩次調用ClassicSingleton.getInstance(),并且把返回的引用存儲在成員變量中。方法testUnique()會檢查這些引用看它們是否相同。例3是這個測試案例的輸出: 例3.是這個測試案例的輸出 Java代碼 1. Buildfile: build.xml    2.     

20、 3. init:    4.      echo Build 20030414 (14-04-2003 03:08)    5.      6. compile:    7.      8. run-test-text:    9.      ja

21、va .INFO main: bgetting singleton./b    10.      java INFO main: bcreated singleton:/b Singletone86f41    11.      java INFO main: .got singleton: Sin

22、gletone86f41    12.      java INFO main: bgetting singleton./b    13.      java INFO main: .got singleton: Singletone86f41    14.      

23、java INFO main: checking singletons for equality    15.      16.      java Time: 0.032    17.      18.      java OK (1 test)&

24、#160; Buildfile: build.xml init: echo Build 20030414 (14-04-2003 03:08) compile: run-test-text: java .INFO main: bgetting singleton./b java INFO main: bcreated singleton:/b Singletone86f41 java INFO main: .got singleton: Singletone86f41 java INFO main: bgetting singleton./b java INFO main: .got

25、 singleton: Singletone86f41 java INFO main: checking singletons for equality java Time: 0.032 java OK (1 test)正如前面的清單所示,例2的簡單測試順利通過-通過ClassicSingleton.getInstance()獲得的兩個單例類的引用確實相同;然而,你要知道這些引用是在單線程中得到的。下面的部分著重于用多線程測試單例類。 多線程因素的考慮 在例1中的ClassicSingleton.getInstance()方法由于下面的代碼而不是線程安全的: Java代碼 1. 1: 

26、;if(instance = null)     2. 2:    instance = new Singleton();    3. 3:    1: if(instance = null) 2: instance = new Singleton(); 3: 如果一個線程在第二行的賦值語句發生之前切換,那么成員變量instance仍然是null,然后另一個線程可能接下來進入到if塊中。在這種情況

27、下,兩個不同的單例類實例就被創建。不幸的是這種假定很少發生,這樣這種假定也很難在測試期間出現(譯注:在這可能是作者對很少出現這種情況而導致無法測試從而使人們放松警惕而感到嘆惜)。為了演示這個線程輪換,我得重新實現例1中的那個類。例4就是修訂后的單例類: 例4.人為安排的方式 Java代碼 1.2.      3. public class Singleton     4.   private static Singleton singleton&

28、#160;= null;    5.   private static Logger logger = Logger.getRootLogger();    6.   private static boolean firstThread = true;    7.      8.   protect

29、ed Singleton()     9.     / Exists only to defeat instantiation.    10.       11.   public static Singleton getInstance()     12.   

30、60;  if(singleton = null)     13.         simulateRandomActivity();    14.         singleton = new Singleton();    15.   

31、0;      16.      ("created singleton: " + singleton);    17.      return singleton;    18.       19.   private

32、0;static void simulateRandomActivity()     20.      try     21.         if(firstThread)     22.            fir

33、stThread = false;    23.            ("sleeping.");    24.      25.            / This nap shoul

34、d give the second thread enough time    26.            / to get by the first thread.    27.           

35、60;  Thread.currentThread().sleep(50);    28.            29.          30.      catch(InterruptedException ex)     31.    

36、;     logger.warn("Sleep interrupted");    32.          33.       34.     public class Singleton private static Singleton singleton = null; private static Logger log

37、ger = 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(); ("created singleton: " + single

38、ton); return singleton; private static void simulateRandomActivity() try if(firstThread) firstThread = false; ("sleeping."); / This nap should give the second thread enough time / to get by the first thread. Thread.currentThread().sleep(50); catch(InterruptedException ex) logger

39、.warn("Sleep interrupted"); 除了在這個清單中的單例類強制使用了一個多線程錯誤處理,例4類似于例1中的單例類。在getInstance()方法第一次被調用時,調用這個方法的線程會休眠50毫秒以便另外的線程也有時間調用getInstance()并創建一個新的單例類實例。當休眠的線程覺醒時,它也會創建一個新的單例類實例,這樣我們就有兩個單例類實例。盡管例4是人為如此的,但它卻模擬了第一個線程調用了getInstance()并在沒有完成時被切換的真實情形。 例5測試了例4的單例類: 例5.失敗的測試 Java代碼 .   

40、;   5. public class SingletonTest extends TestCase     6.    private static Logger logger = Logger.getRootLogger();    7.    private static Singleton singleton 

41、= null;    8.      9.    public SingletonTest(String name)     10.       super(name);    11.        12.    public void&#

42、160;setUp()     13.       singleton = null;    14.        15.    public void testUnique() throws InterruptedException     16.   &#

43、160;   / Both threads call Singleton.getInstance().    17.       Thread threadOne = new Thread(new SingletonTestRunnable(),    18.         &

44、#160;    threadTwo = new Thread(new SingletonTestRunnable();    19.      20.       threadOne.start();    21.       threadTwo.start();   

45、0;22.      23.       threadOne.join();    24.       threadTwo.join();    25.        26.    private static class SingletonTestRunna

46、ble implements Runnable     27.       public void run()     28.          / Get a reference to the singleton.    29. 

47、0;        Singleton s = Singleton.getInstance();    30.      31.          / Protect singleton member variable from    32. 

48、60;        / multithreaded access.    33.          synchronized(SingletonTest.class)     34.             if(si

49、ngleton = null) / If local reference is null.    35.                singleton = s;     / .set it to the single

50、ton    36.              37.          / Local reference must be equal to the one and    38.      

51、;    / only instance of Singleton; otherwise, we have two    39.                   / Singleton instances.    4

52、0.          Assert.assertEquals(true, s = singleton);    41.           42.        43.     public class SingletonTest extends TestCase p

53、rivate 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 = n

54、ew 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 = Sing

55、leton.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,

56、 we have two / Singleton instances. Assert.assertEquals(true, s = singleton); 例5的測試案例創建兩個線程,然后各自啟動,等待完成。這個案例保持了一個對單例類的靜態引用,每個線程都會調用Singleton.getInstance()。如果這個靜態成員變量沒有被設置,那么第一個線程就會將它設為通過調用getInstance()而得到的引用,然后這個靜態變量會與一個局部變量比較是否相等。 在這個測試案例運行時會發生一系列的事情:第一個線程調用getInstance(),進入if塊,然后休眠;接著,第二個線程也調用getIn

57、stance()并且創建了一個單例類的實例。第二個線程會設置這個靜態成員變量為它所創建的引用。第二個線程檢查這個靜態成員變量與一個局部備份的相等性。然后測試通過。當第一個線程覺醒時,它也會創建一個單例類的實例,并且它不會設置那個靜態成員變量(因為第二個線程已經設置過了),所以那個靜態變量與那個局部變量脫離同步,相等性測試即告失敗。例6列出了例5的輸出: 例6.例5的輸出 Java代碼 1. Buildfile: build.xml    2. init:    3.      

58、;echo Build 20030414 (14-04-2003 03:06)    4. compile:    5. run-test-text:    6. INFO Thread-1: sleeping.    7. INFO Thread-2: created singleton: Singleton7e5cbd    8. INFO&#

59、160;Thread-1: created singleton: Singleton704ebb    9.10. 47)    11. 282)    12. 64)    13. 149)    14. 155)    15.    at SingletonTest$SingletonTestRunnable.run(Unknown 

60、Source)    16. 554)    17.      java .    18.      java Time: 0.577    19.      20.      java OK (1 test) 

61、0; 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: Singleton7e5cbd INFO Thread-1: created singleton: Singleton704ebb at SingletonTest$SingletonTestRunnable.run(Unknown Source) java . java Time:

62、0.577 java OK (1 test) 到現在為止我們已經知道例4不是線程安全的,那就讓我們看看如何修正它。 同步 要使例4的單例類為線程安全的很容易-只要像下面一個同步化getInstance()方法: Java代碼 1. public synchronized static Singleton getInstance()     2.    if(singleton = null)     3.  

63、0;    simulateRandomActivity();    4.       singleton = new Singleton();    5.        6.    ("created singleton: " + si

64、ngleton);    7.    return singleton;    8.    public synchronized static Singleton getInstance() if(singleton = null) simulateRandomActivity(); singleton = new Singleton(); ("created singleton: " + singleton); retu

65、rn singleton; 在同步化getInstance()方法后,我們就可以得到例5的測試案例返回的下面的結果: Java代碼 1. Buildfile: build.xml    2.      3. init:    4.      echo Build 20030414 (14-04-2003 03:15)    5.     

66、 6. compile:    7.     javac Compiling 2 source files    8.      9. run-test-text:    10. INFO Thread-1: sleeping.    11. INFO Thread-1: created singl

67、eton: Singletonef577d    12. INFO Thread-2: created singleton: Singletonef577d    13.      java .    14.      java Time: 0.513    15.    

68、60; 16.      java OK (1 test)   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: Singletonef577d INFO Thread-

69、2: created singleton: Singletonef577d java . java Time: 0.513 java OK (1 test) 這此,這個測試案例工作正常,并且多線程的煩惱也被解決;然而,機敏的讀者可能會認識到getInstance()方法只需要在第一次被調用時同步。因為同步的性能開銷很昂貴(同步方法比非同步方法能降低到100次左右),或許我們可以引入一種性能改進方法,它只同步單例類的getInstance()方法中的賦值語句。 一種性能改進的方法 尋找一種性能改進方法時,你可能會選擇像下面這樣重寫getInstance()方法: Java代碼 1. public

70、 static Singleton getInstance()     2.    if(singleton = null)     3.       synchronized(Singleton.class)      4.         

71、60;singleton = new Singleton();    5.           6.        7.    return singleton;    8.    public static Singleton getInstance() if(singleton = n

72、ull) synchronized(Singleton.class) singleton = new Singleton(); return singleton; 這個代碼片段只同步了關鍵的代碼,而不是同步整個方法。然而這段代碼卻不是線程安全的。考慮一下下面的假定:線程1進入同步塊,并且在它給singleton成員變量賦值之前線程1被切換。接著另一個線程進入if塊。第二個線程將等待直到第一個線程完成,并且仍然會得到兩個不同的單例類實例。有修復這個問題的方法嗎?請讀下去。 雙重加鎖檢查 初看上去,雙重加鎖檢查似乎是一種使懶漢式實例化為線程安全的技術。下面的代碼片段展示了這種技術: Java代碼

73、1. public static Singleton getInstance()     2.   if(singleton = null)     3.      synchronized(Singleton.class)     4.        if(singleton 

74、= null)     5.          singleton = new Singleton();    6.            7.         8.       

75、9.   return singleton;    10.    public static Singleton getInstance() if(singleton = null) synchronized(Singleton.class) if(singleton = null) singleton = new Singleton(); return singleton; 如果兩個線程同時訪問getInstance()方法會發生什么?想像一下線程1進行同步塊馬上又被切換。接著,第二個線程進入if 塊。

76、當線程1退出同步塊時,線程2會重新檢查看是否singleton實例仍然為null。因為線程1設置了singleton成員變量,所以線程2的第二次檢查會失敗,第二個單例類實例也就不會被創建。似乎就是如此。 不幸的是,雙重加鎖檢查不會保證正常工作,因為編譯器會在Singleton的構造方法被調用之前隨意給singleton賦一個值。如果在singleton引用被賦值之后而被初始化之前線程1被切換,線程2就會被返回一個對未初始化的單例類實例的引用。 一個改進的線程安全的單例模式實現 例7列出了一個簡單、快速而又是線程安全的單例模式實現: 例7.一個簡單的單例類 Java代碼 1. public

77、60;class Singleton     2.    public final static Singleton INSTANCE = new Singleton();    3.    private Singleton()     4.         

78、; / Exists only to defeat instantiation.    5.           6.    public class Singleton public final static Singleton INSTANCE = new Singleton(); private Singleton() / Exists only to defeat instant

79、iation. 這段代碼是線程安全的是因為靜態成員變量一定會在類被第一次訪問時被創建。你得到了一個自動使用了懶漢式實例化的線程安全的實現;你應該這樣使用它: Java代碼 1. Singleton singleton = Singleton.INSTANCE;    2. singleton.dothis();    3. singleton.dothat();    4. .    Singleton singleton = Singleton

80、.INSTANCE; singleton.dothis(); singleton.dothat(); . 當然萬事并不完美,前面的Singleton只是一個折衷的方案;如果你使用那個實現,你就無法改變它以便后來你可能想要允許多個單例類的實例。用一種更折哀的單例模式實現(通過一個getInstance()方法獲得實例)你可以改變這個方法以便返回一個唯一的實例或者是數百個實例中的一個你不能用一個公開且是靜態的(public static)成員變量這樣做 你可以安全的使用例的單例模式實現或者是例的帶一個同步的getInstance()方法的實現然而,我們必須要研究另一個問題:你必須在編譯期指定這個單

81、例類,這樣就不是很靈活一個單例類的注冊表會讓我們在運行期指定一個單例類 使用注冊表 使用一個單例類注冊表可以: 在運行期指定單例類 防止產生多個單例類子類的實例 在例8的單例類中,保持了一個通過類名進行注冊的單例類注冊表: 例8 帶注冊表的單例類 Java代碼 1.2.3.      4. public class Singleton     5.    private static HashMap map = ne

82、w HashMap();    6.    private static Logger logger = Logger.getRootLogger();    7.      8.    protected Singleton()     9.       / 

83、;Exists only to thwart instantiation    10.        11.    public static synchronized Singleton getInstance(String classname)     12.       if(cla

84、ssname = null) throw new IllegalArgumentException("Illegal classname");    13.          Singleton singleton = (Singleton)map.get(classname);    14.      15.       if(singleton != null)     16.          ("got singleton from&

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論