1. 程式人生 > >確保物件的唯一性——單例模式 (三)

確保物件的唯一性——單例模式 (三)

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

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

      現在我們對負載均衡器的實現程式碼進行再次分析,當第一次呼叫getLoadBalancer()方法建立並啟動負載均衡器時,instance物件為null值,因此係統將執行程式碼instance= new LoadBalancer()

,在此過程中,由於要對LoadBalancer進行大量初始化工作,需要一段時間來建立LoadBalancer物件。而在此時,如果再一次呼叫getLoadBalancer()方法(通常發生在多執行緒環境中),由於instance尚未建立成功,仍為null值,判斷條件(instance== null)為真值,因此程式碼instance= new LoadBalancer()將再次執行,導致最終建立了多個instance物件,這違背了單例模式的初衷,也導致系統執行發生錯誤。

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

1.餓漢式單例類

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

       從圖3-4中可以看出,由於在定義靜態變數的時候例項化單例類,因此在類載入的時候就已經建立了單例物件,程式碼如下所示:
class EagerSingleton { 
    private static final EagerSingleton instance = new EagerSingleton(); 
    private EagerSingleton() { } 

    public static EagerSingleton getInstance() {
        return instance; 
    }   
}
      當類被載入時,靜態變數instance會被初始化,此時類的私有建構函式會被呼叫,單例類的唯一例項將被建立。如果使用餓漢式單例來實現負載均衡器LoadBalancer類的設計,則不會出現建立多個單例物件的情況,可確保單例物件的唯一性。

2.懶漢式單例類與執行緒鎖定

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

     從圖3-5中可以看出,懶漢式單例在第一次呼叫getInstance()方法時例項化,在類載入時並不自行例項化,這種技術又稱為延遲載入(Lazy Load)技術,即需要的時候再載入例項,為了避免多個執行緒同時呼叫getInstance()方法,我們可以使用關鍵字synchronized,程式碼如下所示:
class LazySingleton { 
    private static LazySingleton instance = null; 

    private LazySingleton() { } 

    synchronized public static LazySingleton getInstance() { 
        if (instance == null) {
            instance = new LazySingleton(); 
        }
        return instance; 
    }
}
該懶漢式單例類在getInstance()方法前面增加了關鍵字synchronized進行執行緒鎖,以處理多個執行緒同時訪問的問題。但是,上述程式碼雖然解決了執行緒安全問題,但是每次呼叫getInstance()時都需要進行執行緒鎖定判斷,在多執行緒高併發訪問環境中,將會導致系統性能大大降低。如何既解決執行緒安全問題又不影響系統性能呢?我們繼續對懶漢式單例進行改進。事實上,我們無須對整個getInstance()方法進行鎖定,只需對其中的程式碼“instance = new LazySingleton();”進行鎖定即可。因此getInstance()方法可以進行如下改進:
public static LazySingleton getInstance() { 
    if (instance == null) {
        synchronized (LazySingleton.class) {
            instance = new LazySingleton(); 
        }
    }
    return instance; 
}
問題貌似得以解決,事實並非如此。如果使用以上程式碼來實現單例,還是會存在單例物件不唯一。原因如下:

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

class LazySingleton { 
    private volatile static LazySingleton instance = null; 

    private LazySingleton() { } 

    public static LazySingleton getInstance() { 
        //第一重判斷
        if (instance == null) {
            //鎖定程式碼塊
            synchronized (LazySingleton.class) {
                //第二重判斷
                if (instance == null) {
                    instance = new LazySingleton(); //建立單例例項
                }
            }
        }
        return instance; 
    }
}

       需要注意的是,如果使用雙重檢查鎖定來實現懶漢式單例類,需要在靜態成員變數instance之前增加修飾符volatile,被volatile修飾的成員變數可以確保多個執行緒都能夠正確處理,且該程式碼只能在JDK 1.5及以上版本中才能正確執行。由於volatile關鍵字會遮蔽Java虛擬機器所做的一些程式碼優化,可能會導致系統執行效率降低,因此即使使用雙重檢查鎖定來實現單例模式也不是一種完美的實現方式。

擴充套件

IBM公司高階軟體工程師Peter    Haggar 2004年在IBM developerWorks上發表了一篇名為《雙重檢查鎖定及單例模式——全面理解這一失效的程式設計習語》的文章,對JDK    1.5之前的雙重檢查鎖定及單例模式進行了全面分析和闡述,參考連結:

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

      餓漢式單例類在類被載入時就將自己例項化,它的優點在於無須考慮多執行緒訪問問題,可以確保例項的唯一性;從呼叫速度和反應時間角度來講,由於單例物件一開始就得以建立,因此要優於懶漢式單例。但是無論系統在執行時是否需要使用該單例物件,由於在類載入時該物件就需要建立,因此從資源利用效率角度來講,餓漢式單例不及懶漢式單例,而且在系統載入時由於需要建立餓漢式單例物件,載入時間可能會比較長。

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