確保對象的惟一性——單例模式 (三)

餓漢式單例與懶漢式單例的討論

      Sunny公司開發人員使用單例模式實現了負載均衡器的設計,可是在實際使用中出現了一個很是嚴重的問題,當負載均衡器在啓動過程當中用戶再次啓動該負載均衡器時,系統無任何異常,但當客戶端提交請求時出現請求分發失敗,經過仔細分析發現原來系統中仍是存在多個負載均衡器對象,致使分發時目標服務器不一致,從而產生衝突。爲何會這樣呢?Sunny公司開發人員百思不得其解。html

      如今咱們對負載均衡器的實現代碼進行再次分析,當第一次調用getLoadBalancer()方法建立並啓動負載均衡器時,instance對象爲null值,所以系統將執行代碼instance= new LoadBalancer(),在此過程當中,因爲要對LoadBalancer進行大量初始化工做,須要一段時間來建立LoadBalancer對象。而在此時,若是再一次調用getLoadBalancer()方法(一般發生在多線程環境中),因爲instance還沒有建立成功,仍爲null值,判斷條件(instance== null)爲真值,所以代碼instance= new LoadBalancer()將再次執行,致使最終建立了多個instance對象,這違背了單例模式的初衷,也致使系統運行發生錯誤。java

      如何解決該問題?咱們至少有兩種解決方案,在正式介紹這兩種解決方案以前,先介紹一下單例類的兩種不一樣實現方式,餓漢式單例類和懶漢式單例類。編程

 

1.餓漢式單例類安全

      餓漢式單例類是實現起來最簡單的單例類,餓漢式單例類結構圖如圖3-4所示:服務器

       從圖3-4中能夠看出,因爲在定義靜態變量的時候實例化單例類,所以在類加載的時候就已經建立了單例對象,代碼以下所示:
[java]  view plain  copy
 
  1. class EagerSingleton {   
  2.     private static final EagerSingleton instance = new EagerSingleton();   
  3.     private EagerSingleton() { }   
  4.   
  5.     public static EagerSingleton getInstance() {  
  6.         return instance;   
  7.     }     
  8. }  
      當類被加載時,靜態變量instance會被初始化,此時類的私有構造函數會被調用,單例類的惟一實例將被建立。若是使用餓漢式單例來實現負載均衡器LoadBalancer類的設計,則不會出現建立多個單例對象的狀況,可確保單例對象的惟一性。
 

2.懶漢式單例類與線程鎖定多線程

      除了餓漢式單例,還有一種經典的懶漢式單例,也就是前面的負載均衡器LoadBalancer類的實現方式。懶漢式單例類結構圖如圖3-5所示:併發

     從圖3-5中能夠看出,懶漢式單例在第一次調用getInstance()方法時實例化,在類加載時並不自行實例化,這種技術又稱爲 延遲加載(Lazy Load)技術,即須要的時候再加載實例,爲了不多個線程同時調用getInstance()方法,咱們可使用 關鍵字synchronized,代碼以下所示:
[java]  view plain  copy
 
  1. class LazySingleton {   
  2.     private static LazySingleton instance = null;   
  3.   
  4.     private LazySingleton() { }   
  5.   
  6.     synchronized public static LazySingleton getInstance() {   
  7.         if (instance == null) {  
  8.             instance = new LazySingleton();   
  9.         }  
  10.         return instance;   
  11.     }  
  12. }  
       該懶漢式單例類在getInstance()方法前面增長了關鍵字synchronized進行線程鎖,以處理多個線程同時訪問的問題。可是,上述代碼雖然解決了線程安全問題,可是每次調用getInstance()時都須要進行線程鎖定判斷,在多線程高併發訪問環境中,將會致使系統性能大大下降。如何既解決線程安全問題又不影響系統性能呢?咱們繼續對懶漢式單例進行改進。事實上,咱們無須對整個getInstance()方法進行鎖定,只需對其中的代碼「instance = new LazySingleton();」進行鎖定便可。所以getInstance()方法能夠進行以下改進:
[java]  view plain  copy
 
  1. public static LazySingleton getInstance() {   
  2.     if (instance == null) {  
  3.         synchronized (LazySingleton.class) {  
  4.             instance = new LazySingleton();   
  5.         }  
  6.     }  
  7.     return instance;   
  8. }  
       問題貌似得以解決,事實並不是如此。若是使用以上代碼來實現單例,仍是會存在單例對象不惟一。緣由以下:

      假如在某一瞬間線程A和線程B都在調用getInstance()方法,此時instance對象爲null值,均能經過instance == null的判斷。因爲實現了synchronized加鎖機制,線程A進入synchronized鎖定的代碼中執行實例建立代碼,線程B處於排隊等待狀態,必須等待線程A執行完畢後才能夠進入synchronized鎖定代碼。但當A執行完畢時,線程B並不知道實例已經建立,將繼續建立新的實例,致使產生多個單例對象,違背單例模式的設計思想,所以須要進行進一步改進,在synchronized中再進行一次(instance == null)判斷,這種方式稱爲雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實現的懶漢式單例類完整代碼以下所示:負載均衡

[java]  view plain  copy
 
  1. class LazySingleton {   
  2.     private volatile static LazySingleton instance = null;   
  3.   
  4.     private LazySingleton() { }   
  5.   
  6.     public static LazySingleton getInstance() {   
  7.         //第一重判斷  
  8.         if (instance == null) {  
  9.             //鎖定代碼塊  
  10.             synchronized (LazySingleton.class) {  
  11.                 //第二重判斷  
  12.                 if (instance == null) {  
  13.                     instance = new LazySingleton(); //建立單例實例  
  14.                 }  
  15.             }  
  16.         }  
  17.         return instance;   
  18.     }  
  19. }  

       須要注意的是,若是使用雙重檢查鎖定來實現懶漢式單例類,須要在靜態成員變量instance以前增長修飾符volatile,被volatile修飾的成員變量能夠確保多個線程都可以正確處理,且該代碼只能在JDK 1.5及以上版本中才能正確執行。因爲volatile關鍵字會屏蔽Java虛擬機所作的一些代碼優化,可能會致使系統運行效率下降,所以即便使用雙重檢查鎖定來實現單例模式也不是一種完美的實現方式。 函數

 

擴展高併發

IBM公司高級軟件工程師Peter    Haggar 2004年在IBM developerWorks上發表了一篇名爲《雙重檢查鎖定及單例模式——全面理解這一失效的編程習語》的文章,對JDK    1.5以前的雙重檢查鎖定及單例模式進行了全面分析和闡述,參考連接:http://www.ibm.com/developerworks/cn/java/j-dcl.html

 

3.餓漢式單例類與懶漢式單例類比較

      餓漢式單例類在類被加載時就將本身實例化,它的優勢在於無須考慮多線程訪問問題,能夠確保實例的惟一性;從調用速度和反應時間角度來說,因爲單例對象一開始就得以建立,所以要優於懶漢式單例。可是不管系統在運行時是否須要使用該單例對象,因爲在類加載時該對象就須要建立,所以從資源利用效率角度來說,餓漢式單例不及懶漢式單例,並且在系統加載時因爲須要建立餓漢式單例對象,加載時間可能會比較長。

      懶漢式單例類在第一次使用時建立,無須一直佔用系統資源,實現了延遲加載,可是必須處理好多個線程同時訪問的問題,特別是當單例類做爲資源控制器,在實例化時必然涉及資源初始化,而資源初始化頗有可能耗費大量時間,這意味着出現多線程同時首次引用此類的機率變得較大,須要經過雙重檢查鎖定等機制進行控制,這將致使系統性能受到必定影響。

【做者:劉偉  http://blog.csdn.net/lovelion

 

public class Singleton //: IDisposable
    {
        private Singleton()
        {

        }

        private static Singleton _Singleton = null;
        private static object Singleton_Lock = new object();

        public static Singleton CreateInstance()
        {
            if (_Singleton == null)//保證初始化以後,再也不等待鎖
            {
                lock (Singleton_Lock)//保證單線程進入
                {
                    Console.WriteLine("進入鎖");
                    if (_Singleton == null)//保證只初始化一次
                    {
                        _Singleton = new Singleton();
                    }
                }
            }
            return _Singleton;
        }
    }
相關文章
相關標籤/搜索