設計模式之單例模式

第一種(懶漢,線程不安全):

Java代碼

public class Singleton {   
    private static Singleton instance;   
    private Singleton (){}   

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

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

這種寫法lazy loading很明顯,但是致命的是在多線程不能正常工作。
第二種(懶漢,線程安全):

Java代碼

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

這種寫法能夠在多線程中很好的工作,而且看起來它也具備很好的lazy loading,但是,遺憾的是,效率很低,99%情況下不需要同步。
第三種(餓漢):

Java代碼

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

這種方式基於classloder機制避免了多線程的同步問題,不過,instance在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance顯然沒有達到lazy loading的效果。
第四種(餓漢,變種):

Java代碼

public class Singleton {   
    private static Singleton instance = null;   
    static {   
    instance = new Singleton();  //通過靜態代碼塊只被加載一次的特性來確保對象只被創建一次,對象引用不被改變
    }   
    private Singleton (){}   
    public static Singleton getInstance() {   
    return this.instance;   
    }   
}  

表面上看起來差別挺大,其實更第三種方式差不多,都是在類初始化即實例化instance。
第五種(靜態內部類):

Java代碼

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

這種方式同樣利用了classloder的機制來保證初始化instance時只有一個線程,它跟第三種和第四種方式不同的是(很細微的差別):第三種和第四種方式是隻要Singleton類被裝載了,那麼instance就會被實例化(沒有達到lazy loading效果),而這種方式是Singleton類被裝載了,instance不一定被初始化。因爲SingletonHolder類沒有被主動使用,只有顯示通過調用getInstance方法時,纔會顯示裝載SingletonHolder類,從而實例化instance。想象一下,如果實例化instance很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時就實例化,因爲我不能確保Singleton類還可能在其他的地方被主動使用從而被加載,那麼這個時候實例化instance顯然是不合適的。這個時候,這種方式相比第三和第四種方式就顯得很合理。
第六種(枚舉):

Java代碼

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

這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,可謂是很堅強的壁壘啊,不過,個人認爲由於1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏,在實際工作中,我也很少看見有人這麼寫過。
第七種(雙重校驗鎖):
Java代碼

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

這個是第二種方式的升級版,俗稱雙重檢查鎖定,詳細介紹請查看:這裏寫鏈接內容
在JDK1.5之後,雙重檢查鎖定才能夠正常達到單例效果。

總結
有兩個問題需要注意:
1.如果單例由不同的類裝載器裝入,那便有可能存在多個單例類的實例。假定不是遠端存取,例如一些servlet容器對每個servlet使用完全不同的類裝載器,這樣的話如果有兩個servlet訪問一個單例類,它們就都會有各自的實例。
2.如果Singleton實現了java.io.Serializable接口,那麼這個類的實例就可能被序列化和復原。不管怎樣,如果你序列化一個單例類的對象,接下來複原多個那個對象,那你就會有多個單例類的實例。
對第一個問題修復的辦法是:

Java代碼

private static Class getClass(String classname)       
                                         throws ClassNotFoundException {      
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();      

      if(classLoader == null)      
         classLoader = Singleton.class.getClassLoader();      

      return (classLoader.loadClass(classname));      
   }      
}  

對第二個問題修復的辦法是:

Java代碼

public class Singleton implements java.io.Serializable {   
   public static Singleton INSTANCE = new Singleton();   

   protected Singleton() {   

   }   
   private Object readResolve() {   
            return INSTANCE;   
      }  
}

對我來說,我比較喜歡第三種和第五種方式,簡單易懂,而且在JVM層實現了線程安全(如果不是多個類加載器環境),一般的情況下,我會使用第三種方式,只有在要明確實現lazy loading效果時纔會使用第五種方式,另外,如果涉及到反序列化創建對象時我會試着使用枚舉的方式來實現單例,不過,我一直會保證我的程序是線程安全的,而且我永遠不會使用第一種和第二種方式,如果有其他特殊的需求,我可能會使用第七種方式,畢竟,JDK1.5已經沒有雙重檢查鎖定的問題了。

public class SingletonClass { 

  private static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}

首先去掉getInstance()的同步操作,然後把同步鎖加載if語句上。但是這樣的修改起不到任何作用:因爲每次調用getInstance()的時候必然要同步,性能問題還是存在。如果……如果我們事先判斷一下是不是爲null再去同步呢?

public class SingletonClass { 

  private static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}

還有問題嗎?首先判斷instance是不是爲null,如果爲null,加鎖初始化;如果不爲null,直接返回instance。

這就是double-checked locking設計實現單例模式。

最後將代碼從源頭檢查
下面我們開始說編譯原理。所謂編譯,就是把源代碼“翻譯”成目標代碼——大多數是指機器代碼——的過程。針對Java,它的目標代碼不是本地機器代碼,而是虛擬機代碼。編譯原理裏面有一個很重要的內容是編譯器優化。所謂編譯器優化是指,在不改變原來語義的情況下,通過調整語句順序,來讓程序運行的更快。這個過程成爲reorder。

要知道,JVM只是一個標準,並不是實現。JVM中並沒有規定有關編譯器優化的內容,也就是說,JVM實現可以自由的進行編譯器優化。

下面來想一下,創建一個變量需要哪些步驟呢?一個是申請一塊內存,調用構造方法進行初始化操作,另一個是分配一個指針指向這塊內存。這兩個操作誰在前誰在後呢?JVM規範並沒有規定。那麼就存在這麼一種情況,JVM是先開闢出一塊內存,然後把指針指向這塊內存,最後調用構造方法進行初始化。

下面我們來考慮這麼一種情況:線程A開始創建SingletonClass的實例,此時線程B調用了getInstance()方法,首先判斷instance是否爲null。按照我們上面所說的內存模型,A已經把instance指向了那塊內存,只是還沒有調用構造方法,因此B檢測到instance不爲null,於是直接把instance返回了——問題出現了,儘管instance不爲null,但它並沒有構造完成,就像一套房子已經給了你鑰匙,但你並不能住進去,因爲裏面還沒有收拾。此時,如果B在A將instance構造完成之前就是用了這個實例,程序就會出現錯誤了!

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

我們在第一個同步塊裏面創建一個臨時變量,然後使用這個臨時變量進行對象的創建,並且在最後把instance指針臨時變量的內存空間。寫出這種代碼基於以下思想,即synchronized會起到一個代碼屏蔽的作用,同步塊裏面的代碼和外部的代碼沒有聯繫。因此,在外部的同步塊裏面對臨時變量sc進行操作並不影響instance,所以外部類在instance=sc;之前檢測instance的時候,結果instance依然是null。
不過,這種想法完全是錯誤的!同步塊的釋放保證在此之前——也就是同步塊裏面——的操作必須完成,但是並不保證同步塊之後的操作不能因編譯器優化而調換到同步塊結束之前進行。因此,編譯器完全可以把instance=sc;這句移到內部同步塊裏面執行。這樣,程序又是錯誤的了!

說了這麼多,難道單例沒有辦法在Java中實現嗎?其實不然!

說了這麼多,難道單例沒有辦法在Java中實現嗎?其實不然!

在JDK 5之後,Java使用了新的內存模型。volatile關鍵字有了明確的語義——在JDK1.5之前,volatile是個關鍵字,但是並沒有明確的規定其用途——被volatile修飾的寫變量不能和之前的讀寫代碼調整,讀變量不能和之後的讀寫代碼調整!因此,只要我們簡單的把instance加上volatile關鍵字就可以了。

public class SingletonClass {

private volatile static SingletonClass instance = null;

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

private SingletonClass() {

}

}

然而,這只是JDK1.5之後的Java的解決方案,那之前版本呢?其實,還有另外的一種解決方案,並不會受到Java版本的影響:

public class SingletonClass { 

  private static class SingletonClassInstance { 
    private static final SingletonClass instance = new SingletonClass(); 
  } 

  public static SingletonClass getInstance() { 
    return SingletonClassInstance.instance; 
  } 

  private SingletonClass() { 

  } 

}

在這一版本的單例模式實現代碼中,我們使用了Java的靜態內部類。這一技術是被JVM明確說明了的,因此不存在任何二義性。在這段代碼中,因爲SingletonClass沒有static的屬性,因此並不會被初始化。直到調用getInstance()的時候,會首先加載SingletonClassInstance類,這個類有一個static的SingletonClass實例,因此需要調用SingletonClass的構造方法,然後getInstance()將把這個內部類的instance返回給使用者。由於這個instance是static的,因此並不會構造多次。

由於SingletonClassInstance是私有靜態內部類,所以不會被其他類知道,同樣,static語義也要求不會有多個實例存在。並且,JSL規範定義,類的構造必須是原子性的,非併發的,因此不需要加同步塊。同樣,由於這個構造是併發的,所以getInstance()也並不需要加同步。

至此,我們完整的瞭解了單例模式在Java語言中的時候,提出了兩種解決方案。個人偏向於第二種,並且Effiective Java也推薦的這種方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章