設計模式學習-1-單例模式

單例模式算是設計模式中最容易理解,也是最容易手寫代碼的模式了吧。可是其中的坑卻很多,因此也常做爲面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。不少都是一些老生常談的問題,但若是你不知道如何建立一個線程安全的單例,不知道什麼是雙檢鎖,那這篇文章可能會幫助到你。java

懶漢式,線程不安全

當被問到要實現一個單例模式時,不少人的第一反應是寫出以下的代碼,包括教科書上也是這樣教咱們的程序員

public class Singleton{
      private static Singleton instance;//聲明一個全局的Singleton 對象,並用private 修飾,只能經過下面的方法獲取
      private Singleton(){ //把構造方法聲明爲私有的,
      }
      
      /**
       * 外部只能經過這個方法調用 
       *
       */
      public static Singleton getInstance(){
         if(instance== null){
            instance ==new Singleton();
         }
         return instance;
      }
}

這段代碼簡單的說明了單例模式的使用方式,並且使用了懶加載模式,可是卻存在致命的問題。假若有2個線程在調用getInstance()方法,  第一個線程面試

執行完《1》處的時候CPU的時間片切換給了第二個線程,第二個線程順利的執行完了getInstance 方法,生成了對象,而後CPU又把時間片切換給了第一個線程,然而第一個線程這個時候並不會再檢查if(instance==null),因此他仍然會再一次的執行《2》再次的生成一個對象。當有多個線程並行調用 getInstance() 的時候,就會建立多個實例。也就是說在多線程下不能正常工做。
設計模式

懶漢式,線程安全

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

public class Singleton{
      private static Singleton instance;//聲明一個全局的Singleton 對象,並用private 修飾,只能經過下面的方法獲取
      private Singleton(){ //把構造方法聲明爲私有的,
      }
      
      /**
       * 外部只能經過這個方法調用 
       *
       */
      public static synchronized Singleton getInstance(){  //添加了同步操做
         if(instance== null){
            instance ==new Singleton();
         }
         return instance;
      }
}

雖然作到了線程安全,而且解決了多實例的問題,可是它並不高效。由於在任什麼時候候只能有一個線程調用 getInstance() 方法。可是同步操做只須要在第一次調用時才被須要,即第一次建立單例實例對象時,這樣子就會在每次建立對象的時候進行同步操做,效率大打折扣。這就引出了雙重檢驗鎖。多線程

雙重檢驗鎖

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

public class Singleton{
      private static Singleton instance;//聲明一個全局的Singleton 對象,並用private 修飾,只能經過下面的方法獲取
      private Singleton(){ //把構造方法聲明爲私有的,
      }
      
      /**
       * 外部只能經過這個方法調用 
       *
       */
      public static Singleton getInstance(){  //添加了同步操做
         synchronized (Singleton.class) {  //仍是沒有太明白
            if(instance== null){
                instance ==new Singleton();
             }
             return instance;
           }
     }
     
}

這段代碼看起來很完美,很惋惜,它是有問題。主要在於instance = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。性能

  1. 給 instance 分配內存優化

  2. 調用 Singleton 的構造函數來初始化成員變量spa

  3. 將instance對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)

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

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

 

 public class Singleton {  //在Java 5之後是沒有任何的問題的!放心的使用

    private volatile static Singleton instance; //聲明成 volatile

    private Singleton (){}



    public static Singleton getSingleton() {

       if (instance == null) {                         

            synchronized (Singleton.class) {

                if (instance == null) {       

                    instance = new Singleton();

                }

            }

        }

        return instance;

    }



}

 

有些人認爲使用 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。

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

餓漢式 static final field

這種方法很是簡單,由於單例的實例被聲明成 static 和 final 變量了,在第一次加載類到內存中時就會初始化,因此建立實例自己是線程安全的。

 public class Singleton{

    //類加載時就初始化

    private static final Singleton instance = new Singleton();

    private Singleton(){}  



    public static Singleton getInstance(){
        return instance;
    }

}

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

靜態內部類 static nested class

這種方法是《Effective Java》上所推薦的,乾淨簡潔利落!

//關於static 和 final 見:http://lavasoft.blog.51cto.com/62575/18771/


public class Singleton{

   private Singleton(){}
   private static final SingletonHoldler{
       // final 修飾表示只能被賦值一次,不能再被賦值了
       //
       private static final Singleton INSTANCE =new Singleton();  
   }
   
   public static final Singleton getInstance(){
       return SingletonHolder.INSTANCE;  
   }


}

 

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

枚舉 Enum

用枚舉寫單例實在太簡單了!這也是它最大的優勢。下面這段代碼就是聲明枚舉實例的一般作法。

public enum EasySingleton{
    INSTANCE;
}

咱們能夠經過EasySingleton.INSTANCE來訪問實例,這比調用getInstance()方法簡單多了。建立枚舉默認就是線程安全的,因此不須要擔憂double checked locking,並且還能防止反序列化致使從新建立新的對象。可是仍是不多看到有人這樣寫,多是由於不太熟悉吧。

總結

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

就我我的而言,通常狀況下直接使用餓漢式就行了,若是明確要求要懶加載(lazy initialization)會傾向於使用靜態內部類,若是涉及到反序列化建立對象時會試着使用枚舉的方式來實現單例。

相關文章
相關標籤/搜索