Java提升篇——單例模式

介紹

   在咱們平常的工做中常常須要在應用程序中保持一個惟一的實例,如:IO處理,數據庫操做等,因爲這些對象都要佔用重要的系統資源,因此咱們必須限制這些實例的建立或始終使用一個公用的實例,這就是咱們今天要介紹的——單例模式(Singleton)。html

  單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於建立型模式,它提供了一種建立對象的最佳方式。java

  這種模式涉及到一個單一的類,該類負責建立本身的對象,同時確保只有單個對象被建立。這個類提供了一種訪問其惟一的對象的方式,能夠直接訪問,不須要實例化該類的對象。程序員

注意:數據庫

  • 一、單例類只能有一個實例。
  • 二、單例類必須本身建立本身的惟一實例。
  • 三、單例類必須給全部其餘對象提供這一實例。

實現

  咱們知道,一個類的對象的產生是由類構造函數來完成的。若是一個類對外提供了public的構造方法,那麼外界就能夠任意建立該類的對象。因此,若是想限制對象的產生,一個辦法就是將構造函數變爲私有的(至少是受保護的),使外面的類不能經過引用來產生對象。同時爲了保證類的可用性,就必須提供一個本身的對象以及訪問這個對象的靜態方法。設計模式

  實現單例模式的思路是:一個類能返回對象一個引用(永遠是同一個)和一個得到該實例的方法(必須是靜態方法,一般使用getInstance這個名稱);當咱們調用這個方法時,若是類持有的引用不爲空就返回這個引用,若是類保持的引用爲空就建立該類的實例並將實例的引用賦予該類保持的引用;同時咱們還將該類的構造函數定義爲私有方法,這樣其餘處的代碼就沒法經過調用該類的構造函數來實例化該類的對象,只有經過該類提供的靜態方法來獲得該類的惟一實例。安全

咱們將建立一個 SingleObject 類。SingleObject 類有它的私有構造函數和自己的一個靜態實例。多線程

SingleObject 類提供了一個靜態方法,供外界獲取它的靜態實例。SingletonPatternDemo,咱們的演示類使用 SingleObject 類來獲取SingleObject 對象。函數

①建立一個 Singleton 類。性能

SingleObject.java優化

public class SingleObject {

   //建立 SingleObject 的一個對象
   private static SingleObject instance = new SingleObject();

   //讓構造函數爲 private,這樣該類就不會被實例化
   private SingleObject(){}

   //獲取惟一可用的對象
   public static SingleObject getInstance(){
      return instance;
   }

   public void showMessage(){
      System.out.println("Hello World!");
   }
}

②從 singleton 類獲取惟一的對象。

SingletonPatternDemo.java

public class SingletonPatternDemo {
   public static void main(String[] args) {

      //不合法的構造函數
      //編譯時錯誤:構造函數 SingleObject() 是不可見的
      //SingleObject object = new SingleObject();

      //獲取惟一可用的對象
      SingleObject object = SingleObject.getInstance();

      //顯示消息
      object.showMessage();
   }
}

輸出結果:

Hello World!

單例模式的幾種實現方式

 一、懶漢式,線程不安全

描述:這種方式是最基本的實現方式,這種實現最大的問題就是不支持多線程。由於沒有加鎖 synchronized,因此嚴格意義上它並不算單例模式。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
     }  
}  

這段代碼簡單明瞭,並且使用了懶加載( lazy loading )模式,可是卻存在致命的問題。當有多個線程並行調用 getInstance() 的時候,就會建立多個實例。也就是說在多線程下不能正常工做。

接下來介紹的幾種實現方式都支持多線程,可是在性能上有所差別。 

二、懶漢式,線程安全

爲了解決上面的問題,最簡單的方法是將整個 getInstance() 方法設爲同步(synchronized)。

描述:這種方式具有很好的 lazy loading,可以在多線程中很好的工做,可是,效率很低,99% 狀況下不須要同步。
優勢:第一次調用才初始化,避免內存浪費。
缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。
getInstance() 的性能對應用程序不是很關鍵(該方法使用不太頻繁)。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
} 

雖然作到了線程安全,而且解決了多實例的問題,可是它並不高效。由於在任什麼時候候只能有一個線程調用 getInstance() 方法。可是同步操做只須要在第一次調用時才被須要(即if語句中判斷 instance 爲null時才調用,不爲null時是直接返回instance的),即第一次建立單例實例對象時。這就引出了雙重檢驗鎖。

三、雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)

雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其爲雙重檢查鎖,由於會有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內。爲何在同步塊內還要再檢驗一次?由於可能會有多個線程一塊兒進入同步塊外的 if,若是在同步塊內不進行二次檢驗的話就會生成多個實例了。

描述:這種方式採用雙鎖機制,安全且在多線程狀況下能保持高性能。
getInstance() 的性能對應用程序很關鍵。

public class Singleton {
    private volatile static Singleton singleton;//聲明成 volatile
    private Singleton() {}

    public static Singleton getSingleton() {
        if (instance == null) {              // Single Checked
            synchronized (Singleton.class) {
                if (instance == null) {      // Double Checked
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}  

注意上面使用了volatile 關鍵字。

若是去掉volatile 關鍵字,它是有問題。主要在於instance = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。

  1. 給 instance 分配內存
  2. 調用 Singleton 的構造函數來初始化成員變量
  3. 將instance對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)

可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。

因此咱們只須要將 instance 變量聲明成 volatile 就能夠了。

有些人認爲使用 volatile 的緣由是可見性,也就是能夠保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但實際上是不對的。使用 volatile 的主要緣由是其另外一個特性:禁止指令重排序優化。也就是說,在 volatile 變量的賦值操做後面會有一個內存屏障(生成的彙編代碼上),讀操做不會被重排序到內存屏障以前。好比上面的例子,取操做必須在執行完 1-2-3 以後或者 1-3-2 以後,不存在執行到 1-3 而後取到值的狀況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變量的寫操做都先行發生於後面對這個變量的讀操做(這裏的「後面」是時間上的前後順序)。

可是特別注意在 Java 5 之前的版本使用了 volatile 的雙檢鎖仍是有問題的。其緣由是 Java 5 之前的 JMM (Java 內存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能徹底避免重排序,主要是 volatile 變量先後的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,因此在這以後才能夠放心使用 volatile。

相信你不會喜歡這種複雜又隱含問題的方式,固然咱們有更好的實現線程安全的單例模式的辦法。

四、餓漢式

餓漢法就是在第一次引用該類的時候就建立對象實例,而無論實際是否須要建立。

 描述:這種方式比較經常使用,但容易產生垃圾對象。
優勢:沒有加鎖,執行效率會提升。
缺點:類加載時就初始化,浪費內存。
它基於 classloder 機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然致使類裝載的緣由有不少種,在單例模式中大多數都是調用 getInstance 方法, 可是也不能肯定有其餘的方式(或者其餘的靜態方法)致使類裝載,這時候初始化 instance 顯然沒有達到 lazy loading 的效果。

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
        return instance;  
    }  
}

這種寫法若是完美的話,就不必在囉嗦那麼多雙檢鎖的問題了。缺點是它不是一種懶加載模式(lazy initialization),單例會在加載類後一開始就被初始化,即便客戶端沒有調用 getInstance()方法。餓漢式的建立方式在一些場景中將沒法使用:譬如 Singleton 實例的建立是依賴參數或者配置文件的,在 getInstance() 以前必須調用某個方法設置參數給它,那樣這種單例寫法就沒法使用了。

五、登記式/靜態內部類

描述:這種方式能達到雙檢鎖方式同樣的功效,但實現更簡單。對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用於靜態域的狀況,雙檢鎖方式可在實例域須要延遲初始化時使用。
這種方式一樣利用了 classloder 機制來保證初始化 instance 時只有一個線程,它跟第 4 種方式不一樣的是:第 4 種方式只要 Singleton 類被裝載了,那麼 instance 就會被實例化(沒有達到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不必定被初始化。由於 SingletonHolder 類沒有被主動使用,只有顯示經過調用 getInstance 方法時,纔會顯示裝載 SingletonHolder 類,從而實例化 instance。想象一下,若是實例化 instance 很消耗資源,因此想讓它延遲加載,另一方面,又不但願在 Singleton 類加載時就實例化,由於不能確保 Singleton 類還可能在其餘的地方被主動使用從而被加載,那麼這個時候實例化 instance 顯然是不合適的。這個時候,這種方式相比第 4 種方式就顯得很合理。

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
}  

這種寫法仍然使用JVM自己機制保證了線程安全問題;因爲 SingletonHolder 是私有的,除了 getInstance() 以外沒有辦法訪問它,所以它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。

六、枚舉Enum

描述:這種實現方式尚未被普遍採用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止屢次實例化。
這種方式是 Effective Java 做者 Josh Bloch 提倡的方式,它不只能避免多線程同步問題,並且還自動支持序列化機制,防止反序列化從新建立新的對象,絕對防止屢次實例化。不過,因爲 JDK1.5 以後才加入 enum 特性,用這種方式寫難免讓人感受生疏,在實際工做中,也不多用。
不能經過 reflection attack 來調用私有構造方法。

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

總結

單例的特色:外界沒法經過構造器來建立對象,該類必須提供一個靜態方法向外界提供該類的惟一實例。

實現一個單例有兩點注意事項,①將構造器私有,不容許外界經過構造器建立對象;②經過公開的靜態方法向外界返回類的惟一實例。

通常來講,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、枚舉。上述所說都是線程安全的實現,文章開頭給出的第1種方法不算正確的寫法。

經驗之談:通常狀況下,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 4種餓漢方式。只有在要明確實現 lazy loading 效果時,纔會使用第 5 種登記方式。若是涉及到反序列化建立對象時,能夠嘗試使用第 6 種枚舉方式。若是有其餘特殊的需求,能夠考慮使用第 3 種雙檢鎖方式。


相關文檔連接:

一、單例模式

二、如何正確地寫出單例模式

三、你真的會寫單例模式嗎——Java實現

四、設計模式(二)——單例模式

相關文章
相關標籤/搜索