java中的單例模式

第二部分  單例模式

定義:保證一個類,只有一個實例存在,同時提供能對該實例加以訪問的全局訪問方法。
        單例模式是一種對象創建型模式,使用單例模式可以保證爲一個類只生成唯一的實例對象。也就是說,在整個程序空間中,該類只存在一個實例
  對象。單例模式的要有三個:1)某個類只有一個實例;2)它必須自行創建這個實例;3)它必須自行向整個系統提供這個實例

2.單例模式深入分析
         單例模式適合於一個類只有一個實例的情況,如窗口管理器,打印緩衝池和文件系統,它們都是原型的例子。典型的情況是,那些對象的類型被遍及一個
         軟件系統的不同對象訪問,因此需要一個全局的訪問指針,這便是衆所周知的單例模式的應用。
         注意:使用單例模式時,構造函數一定是private類型的。
         在計算機系統中,需要管理的資源包括軟件外部資源,例如,每臺計算機可以有若干個打印機,但是有一個Printer Spooler,以避免兩個打印作業同時
     傳輸到傳真卡中的情況。每臺計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。需要管理的軟件
     內部資源包括負責記錄網站來訪人數的部件,記錄軟件系統內部事件、出錯信息的部件,或是對系統的表現進行檢查的部件等,這些部件都必須集中管理。

1) 單例模式的第一個版本,採用的是"餓漢式",也就是當類加載進來時就立即實例化當前類的對象,這種方式比較消耗計算機的資源,代碼如下:
package com.example.singleton;
public class SingletonOne {
         public static final SingletonOne singletonOne=new SingletonOne();

          public static SingletonOne getSingletonOne(){
                      return singletonOne;
          }
}
2)單例模式提供的第二個版本,"懶漢式",在單線程中能夠非常好的工作,但是在多線程中存在線程安全問題
 package com.example.singleton;
  public class SingletonTwo {
      
            private static SingletonTwo singletonTwo;
             private SingletonTwo(){

               }

            public static SingletonTwo getSingletonTwo(){
                          if(null == singletonTwo){
                                        singletonTwo = new SingletonTwo();
            }
                     return singletonTwo;
          }
}

3)爲解決多線程問題,採用對函數進行同步的方式,但是比較浪費資源,因爲每次都要進行同步檢查,而實際中真正需要好只是在第一次實例化時檢查
即在實例化的時候使用synchronized 關鍵字來實現同步:
public static synchronized SingletonThree getSingletonThree(){
          if(null == singletonThree){
                  singletonThree = new SingletonThree();
          }
          return singletonThree;
}

4)既可以解決懶漢式,又可以解決多線程問題,還可以資源浪費的現象,看上去是一種不同的選擇
public static SingletonFour getSingletonFour(){

            if(null == singletonFour){
                             synchronized(SingletonFour.class){
                                                if(null == singletonFour){
                                                              singletonFour = new SingletonFour();
                                                 }
                            }
             }
            return singletonFour;
}

另外也可以看一篇文章:

一.  單例模式簡介

      單例(Singleton)模式是使用最廣泛的設計模式。其思想意圖是保證一個類只有一個實例,並且提供類對象的全程訪問。單實例對象應用的範圍很廣:如GUI應用必須是單鼠標,MODEM的聯接需要一條且只需要一條電話線,操作系統只能有一個窗口管理器,一臺PC連一個鍵盤。使用全程對象能夠保證方便地訪問實例,但是不能保證只聲明一個對象-也就是說除了一個全程實例外,仍然能創建相同類的本地實例。單實例模式通過類本身來管理其唯一實例,這種特性提供了問題的解決辦法。唯一的實例是類的一個普通對象,但設計這個類時,讓它只能創建一個實例並提供對此實例的全程訪問。唯一實例類Singleton在靜態成員函數中隱藏創建實例的操作。

二. 單例模式實現

     單例模式在java裏有兩個實現方式:1、懶漢模式; 2、餓漢模式。

     代碼1、懶漢模式

  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的懶漢模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class Singleton   
  9. {  
  10.     private static Singleton singleton;  
  11.       
  12.     private Singleton()  
  13.     {     
  14.     }  
  15.       
  16.     public static Singleton getInstance()  
  17.     {  
  18.         if(null == singleton)  
  19.         {  
  20.             singleton = new Singleton();  
  21.         }  
  22.         return singleton;  
  23.     }  
  24. }  

     代碼2、餓漢模式

  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的餓漢模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class AnotherSingleton  
  9. {  
  10.     private static AnotherSingleton singleton = new AnotherSingleton();   
  11.       
  12.     private AnotherSingleton()  
  13.     {  
  14.     }  
  15.       
  16.     public static AnotherSingleton getInstance()  
  17.     {  
  18.         return singleton;  
  19.     }  
  20. }  

    代碼3. 測試代碼 

  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Test Singleton 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class SingletonTest  
  9. {  
  10.     public static void main(String[] args)  
  11.     {  
  12.         Singleton s1 = Singleton.getInstance();  
  13.         Singleton s2 = Singleton.getInstance();  
  14.           
  15.         System.out.println(s1 == s2);   //return true  
  16.           
  17.         AnotherSingleton s3 = AnotherSingleton.getInstance();  
  18.         AnotherSingleton s4 = AnotherSingleton.getInstance();  
  19.           
  20.         System.out.println(s3 == s4);   //return true  
  21.     }  
  22. }  

     兩種實現模式的比較:
     1、相同點:兩種方式的構造函數都是私有的,對外的接口都是工廠方法。

    2、不同點:餓漢式是在類裝載的時候直接得到該類的實例,可以說是前期綁定的;懶漢式是後期綁定的,類加載的時候uniSingleton是空的,在需要的時候才被創建且僅創建一次。餓漢式的速度快,效率高,但是耗費系統資源;懶漢式則相反。

    注意:懶漢式還存在一個問題,就是後期綁定不能確保對象只能被實例化一次。這就涉及到線程安全。

三. 單例模式的線程安全性探討

       如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

       線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。

       單例模式下最關心的就是這個線程安全的問題了。上面我們提到懶漢模式會有線程安全的問題。當引入多線程時,就必須通過同步來保護getInstance() 方法。如果不保護 getInstance() 方法,則可能返回 Singleton 對象的兩個不同的實例。假設兩個線程併發調用 getInstance() 方法並且按以下順序執行調用:

       1.   線程 1 調用 getInstance() 方法並決定 instance  //1 處爲 null

      2.   線程 1 進入 if 代碼塊,但在執行 //2 處的代碼行時被線程 2 預佔。

      3.   線程 2 調用 getInstance() 方法並在 //1 處決定 instance  null

      4.   線程 2 進入 if 代碼塊並創建一個新的 Singleton 對象並在 //2 處將變量 instance 分配給這個新對象。

      5.   線程 2  //3 處返回 Singleton 對象引用。

      6.   線程 2 被線程 1 預佔。

      7.   線程 1 在它停止的地方啓動,並執行 //2 代碼行,這導致創建另一個 Singleton 對象。

      8.   線程 1  //3 處返回這個對象。

    這樣,getInstance()方法就創建了2Singleton對象,與單例模式的意圖相違背。通過使用synchronized同步getInstance() 方法從而在同一時間只允許一個線程執行代碼。代碼如下:

    代碼4

   

  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的懶漢模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class Singleton   
  9. {  
  10.     private static Singleton singleton;  
  11.       
  12.     private Singleton()  
  13.     {     
  14.     }  
  15.       
  16.     public static synchronized Singleton getInstance()  
  17.     {  
  18.         if(null == singleton)  
  19.         {  
  20.             singleton = new Singleton();  
  21.         }  
  22.         return singleton;  
  23.     }  
  24. }  

    此代碼針對多線程訪問 getInstance() 方法運行得很好。然而,分析這段代碼,您會意識到只有在第一次調用方法時才需要同步。由於只有第一次調用執行了 //2 處的代碼,而只有此行代碼需要同步,因此就無需對後續調用使用同步。所有其他調用用於決定 instance 是非 null 的,並將其返回。多線程能夠安全併發地執行除第一次調用外的所有調用。儘管如此,由於該方法是 synchronized 的,需要爲該方法的每一次調用付出同步的代價,即使只有第一次調用需要同步。

因爲代碼4中只有//2需要同步,我們可以只將其包裝到一個同步塊中。得到的代碼如下:

  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的懶漢模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class Singleton   
  9. {  
  10.     private static Singleton singleton;  
  11.       
  12.     private Singleton()  
  13.     {     
  14.     }  
  15.       
  16.     public static Singleton getInstance()  
  17.     {  
  18.         if(null == singleton)  
  19.         {  
  20.             synchronized (Singleton.class)  
  21.             {  
  22.                 singleton = new Singleton();  
  23.             }  
  24.         }  
  25.         return singleton;  
  26.     }  
  27. }  

    可是代碼5出現了代碼1同樣的問題。當 instance  null 時,兩個線程可以併發地進入 if 語句內部。然後,一個線程進入 synchronized 塊來初始化 instance,而另一個線程則被阻斷。當第一個線程退出 synchronized 塊時,等待着的線程進入並創建另一個 Singleton 對象。注意:當第二個線程進入 synchronized 塊時,它並沒有檢查 instance 是否非 null

爲了解決代碼5出現的問題,我們對instance進行兩次檢查,即“雙重檢查鎖定”。代碼如下:

    代碼6

  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的懶漢模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class Singleton   
  9. {  
  10.     private static Singleton singleton;  
  11.       
  12.     private Singleton()  
  13.     {     
  14.     }  
  15.       
  16.     public static Singleton getInstance()  
  17.     {  
  18.         if(null == singleton)  
  19.         {  
  20.             synchronized (Singleton.class)  
  21.             {  
  22.                 if(null == singleton)  
  23.                 {  
  24.                     singleton = new Singleton();  
  25.                 }  
  26.             }  
  27.         }  
  28.         return singleton;  
  29.     }  
  30. }  

    雙重檢查鎖定在理論上能夠保證代碼6只創建一個Singleton對象。假設有下列事件序列:

    1. 線程 1 進入 getInstance() 方法。

      2. 由於 instance  null,線程 1  //1 處進入 synchronized 塊。

      3. 線程 1 被線程 2 預佔。

      4. 線程 2 進入 getInstance() 方法。

      5. 由於 instance 仍舊爲 null,線程 2 試圖獲取 //1 處的鎖。然而,由於線程 1 持有該鎖,線程 2  //1 處阻塞。

      6. 線程 2 被線程 1 預佔。

      7. 線程 1 執行,由於在 //2 處實例仍舊爲 null,線程 1 還創建一個 Singleton 對象並將其引用賦值給 instance

      8. 線程 1 退出 synchronized 塊並從 getInstance() 方法返回實例。

      9. 線程 1 被線程 2 預佔。

     10. 線程 2 獲取 //1 處的鎖並檢查 instance 是否爲 null

     11. 由於 instance 是非 null 的,並沒有創建第二個 Singleton 對象,由線程 1 創建的對象被返回。

    看起來,雙重檢查鎖定既解決了代碼4的效率低下問題,又解決了代碼5的線程安全性問題。但是它並不能保證它會在單處理器或多處理器計算機上順利運行,根源在於 Java 平臺內存模型。深入瞭解可以參考相關資料。

四.單例模式的選擇

    無論以何種形式,都不應使用雙重檢查鎖定,因爲您不能保證它在任何 JVM 實現上都能順利運行。

    如果只在單線程環境下運行,最好使用代碼1

    如果涉及到多線程環境,最好使用代碼2,也可以使用代碼4(儘管效率低下,但可以保證線程同步)。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章