設計模式之-單例模式

java設計模式之--單例模式

單例模式

單例模式限制類的實例和確保java類在java虛擬機中只有一個實例的存在。java

單例類必須提供一個全局的訪問來獲取類的實例。設計模式

單例模式用來日誌,驅動對象,緩存和線程池。緩存

單例設計模式也用在其餘設計模式,例如抽象工廠,建造者,原型,門面等設計模式。安全

單例模式還用在覈心java中,例如java.lang.Runtime, java.awt.Desktop多線程

java單例模式

爲了實現Singleton模式,咱們有不一樣的方法,但它們都有如下共同的概念。併發

  • 私有構造方法限制從其餘類初始化類的實例。
  • 私有靜態變量與該類的實例相同。
  • 公有靜態方法返回類的實例,這是提供給外部訪問的全局訪問點來獲取單例類的實例。在如下的章節,咱們將學習單例模式的不一樣實現方法。

常見的實現方式以下

  • 餓漢式
  • 懶漢式
  • volatile雙重檢查鎖機制
  • 靜態內部類
  • 枚舉(天生單例)

餓漢式

顧名思義,餓漢式就是第一次引用該類的時候就建立實例對象,而不論是否須要。代碼以下:ide

public class Singleton {   
        private static Singleton singleton = new Singleton();
        private Singleton() {}
        public static Singleton getSignleton(){
            return singleton;
        }
    }

優缺點:這樣作的好處是代碼簡單,可是沒法作到延遲加載。可是不少時候咱們但願可以延遲加載,從而減少負載,因此就有了下面的懶漢式;高併發

懶漢式

單線程寫法
這種寫法是最簡單的,由私有構造器和一個公有靜態工廠方法構成,在工廠方法中對singleton進行null判斷,若是是null就new一個出來,最後返回singleton對象。
這種方法能夠實現延時加載,可是有一個致命弱點:
線程不安全。若是有兩條線程同時調用getSingleton()方法,就有很大可能致使重複建立對象。性能

public class Singleton {
   private static Singleton singleton = null;
   
   private Singleton(){}
   
   public static Singleton getSingleton() {
       if(singleton == null) {
         singleton = new Singleton();
       }
       return singleton;
   }
}

線程安全寫法
這種寫法考慮了線程安全,將對singleton的null判斷以及new的部分使用synchronized進行加鎖。同時,對singleton對象使用volatile關鍵字進行限制,保證其對全部線程的可見性,而且禁止對其進行指令重排序優化。如此便可從語義上保證這種單例模式寫法是線程安全的。注意,這裏說的是語義上,實際使用中仍是存在小坑的,會在後文寫到。學習

public class Singleton {
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }    
}

雙重檢查鎖

雖然上面這種寫法是能夠正確運行的,可是其效率低下,仍是沒法實際應用。由於每次調用getSingleton()方法,都必須在synchronized這裏進行排隊,而真正遇到須要new的狀況是很是少的。因此,就誕生了第三種寫法:

public class Singleton {
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

這種寫法被稱爲「雙重檢查鎖」,顧名思義,就是在getSingleton()方法中,進行兩次null檢查。看似畫蛇添足,但實際上卻極大提高了併發度,進而提高了性能。爲何能夠提升併發度呢?就像上文說的,在單例中new的狀況很是少,絕大多數都是能夠並行的讀操做。所以在加鎖前多進行一次null檢查就能夠減小絕大多數的加鎖操做,執行效率提升的目的也就達到了;

雙重檢查鎖機制的坑

那麼,這種寫法是否是絕對安全呢?前面說了,從語義角度來看,並無什麼問題。可是其實仍是有坑。

  • 說這個坑以前咱們要先來看看volatile這個關鍵字。其實這個關鍵字有兩層語義。
  • 第一層語義你們相對比較熟悉,可見性。可見性是指在一個線程中對該變量的修改由工做內存(Work Memory)寫回主內存(Main Memory),因此會立刻反應到其餘線程的讀寫操做中。順便一提,工做內存和主內存能夠近似理解成電腦中的高速緩存和主存,工做內存是線程獨享的,主存是線程共享的
  • volatile的第二層語義是防止指令重排。你們知道咱們寫的代碼(尤爲是多線程代碼),因爲編譯器優化,在實際執行的時候可能和咱們編寫的順序不一樣。編譯器只保證程序執行結果和源代碼相同,卻不保證明際指令的順序和源代碼相同。這在單線程沒什麼問題,然而一旦引入多線程,這種亂序就可能致使嚴重問題。volatile關鍵字就能夠從語義上解決這個問題。

注意,禁止指令重排優化這條語義直到jdk1.5之後才能正確工做。此前的JDK中即便將變量聲明爲volatile也沒法徹底避免重排序所致使的問題。因此,在jdk1.5版本前,雙重檢查鎖形式的單例模式是沒法保證線程安全的。

靜態內部類

那麼,有沒有一種延時加載,而且能保證線程安全的簡單寫法呢?咱們能夠把Singleton實例放到一個靜態內部類中,這樣就避免了靜態實例在Singleton類加載的時候就建立對象,而且因爲靜態內部類只會被加載一次,因此這種寫法也是線程安全的:

public class Singleton {

    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

可是,上面提到的全部實現方式都有兩個共同的缺點:

  1. 都須要額外的工做(Serializable、transient、readResolve())來實現序列化,不然每次反序列化一個序列化的對象實例時都會建立一個新的實例。
  2. 可能會有人使用反射強行調用咱們的私有構造器(若是要避免這種狀況,能夠修改構造器,讓它在建立第二個實例的時候拋異常)。

枚舉寫法

固然,還有一種更加優雅的方法來實現單例模式,那就是枚舉寫法:

public enum SingleEnum {
    NEW_INSTANCE {
        @Override
        protected void doSomething() {
            System.out.println("----業務方法調用----");
        }
    };

    SingleEnum() {
    }

    /**
     * 業務方法定義
     */
    protected abstract void doSomething();

    public static void main(String[] args) {
        SingleEnum.NEW_INSTANCE.doSomething();
    }
}

使用枚舉除了線程安全和防止反射強行調用構造器以外,還提供了自動序列化機制,防止反序列化的時候建立新的對象。所以,Effective Java推薦儘量地使用枚舉來實現單例。

總結

代碼沒有一勞永逸的寫法,只有在特定條件下最合適的寫法。在不一樣的平臺、不一樣的開發環境(尤爲是jdk版本)下,天然有不一樣的最優解(或者說較優解)。
好比枚舉,雖然Effective Java中推薦使用,可是在Android平臺上倒是不被推薦的。在這篇Android Training中明確指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再好比雙重檢查鎖法,不能在jdk1.5以前使用,而在Android平臺上使用就比較放心了(通常Android都是jdk1.6以上了,不只修正了volatile的語義問題,還加入了很多鎖優化,使得多線程同步的開銷下降很多)。

最後,無論採起何種方案,請時刻牢記單例的三大要點:

  • 線程安全
  • 延遲加載
  • 序列化與反序列化安全

參考資料

《Effective Java(第二版)》 《深刻理解Java虛擬機——JVM高級特性與最佳實踐(第二版)》
相關文章
相關標籤/搜索