淺談設計模式--單例模式(Singleton Pattern)

題外話:很久沒寫blog,作知識概括整理了。原本設計模式就是個坑,各類文章也寫爛了。不過,不是本身寫的東西,缺乏點知識的存在感。目前還沒作到光看即能記住,得寫。因此準備跳入設計模式這個大坑。html

 

開篇先貢獻給java

單例模式(Singleton Pattern)

 

目的:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。程序員

其實單例模式應用不少,我也不陌生,有時候一些本身定義的Controller等,都會選擇單例模式去實現,而自己java.lang.Runtime類的源碼也使用了單例模式(Jdk7u40):編程

public class Runtime {

    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() { 
      return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    
    ......

}

然而,由於涉及到多線程編程,單例模式仍是有很多值得注意的地方,請看下面的各類實現。設計模式

 

1.最簡單實現:

/**
 * @author YYC
 * lazy-loading but NOT thread-safe
 */
public class SingletonExample {

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

這是單例模式最簡單最直接的實現方法。懶漢式(lazy-loading)實現,但缺點很明顯:線程不安全,不能用於多線程環境安全

 

2.同步方法實現:

/**
 * @author YYC
 * Thread-safe but bad performance
 */
public class SingletonExample {

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

同步getInstance()這個方法,能夠保證線程安全。不過代價是性能會受到,由於大部分時間的操做其實不須要同步。多線程

 

3. Double-Checked Locking實現(DCL):

/**
 * @author YYC
 * Double-Checked Locking
 */
public class SingletonExample {

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

直接同步整個getInstance()方法產生性能低下的緣由是,在判斷(instance==null)時,全部線程都必須等待。而(instance==null)並不是是常有狀況,每次判斷都必須等待,會形成阻塞。所以,有了這種雙重檢測的實現方法,待檢查到實例沒建立後(instance=null),再進行同步,而後再檢查一次確保實例沒建立。框架

 

在同步塊裏,再斷定一次,是爲了不線程A準備拿到鎖,而線程B建立完instance後準備釋放鎖的狀況。若是在同步塊裏沒有再次斷定,那麼線程A極可能會又建立一個實例。性能

另外,再引用IcyFenix文章裏面的一段話,會解釋清楚雙鎖檢測的侷限性:測試

咱們來看看這個場景:假設線程一執行到instance = new SingletonExample()這句,這裏看起來是一句話,但實際上它並非一個原子操做(原子操做的意思就是這條語句要麼就被執行完,要麼就沒有被執行過,不能出現執行了一半這種情形)。事實上高級語言裏面非原子操做有不少,咱們只要看看這句話被編譯後在JVM執行的對應彙編代碼就發現,這句話被編譯成8條彙編指令,大體作了3件事情:

 

1.給SingletonExample的實例分配內存。

2.初始化SingletonExample的構造器

3.將instance對象指向分配的內存空間(注意到這步instance就非null了)。

 

可是,因爲Java編譯器容許處理器亂序執行(out-of-order),以及JDK1.5以前JMM(Java Memory Medel)中Cache、寄存器到主內存回寫順序的規定,上面的第二點和第三點的順序是沒法保證的,也就是說,執行順序多是1-2-3也多是1-3-2,若是是後者,而且在3執行完畢、2未執行以前,被切換到線程二上,這時候instance由於已經在線程一內執行過了第三點,instance已是非空了,因此線程二直接拿走instance,而後使用,而後瓜熟蒂落地報錯,並且這種難以跟蹤難以重現的錯誤估計調試上一星期都未必能找得出來,真是一茶几的杯具啊。

 

DCL的寫法來實現單例是不少技術書、教科書(包括基於JDK1.4之前版本的書籍)上推薦的寫法,其實是不徹底正確的。的確在一些語言(譬如C語言)上DCL是可行的,取決因而否能保證二、3步的順序。在JDK1.5以後,官方已經注意到這種問題,所以調整了JMM、具體化了volatile關鍵字,所以若是JDK是1.5或以後的版本,只須要將instance的定義改爲「private volatile static SingletonExample instance = null;」就能夠保證每次都去instance都從主內存讀取,就可使用DCL的寫法來完成單例模式。固然volatile或多或少也會影響到性能,最重要的是咱們還要考慮JDK1.42以及以前的版本,因此本文中單例模式寫法的改進還在繼續。

 

4. 餓漢式實現(Hungry man):

/**
 * @author YYC
 * Hungry man. Using class loader to make it thread-safe 
 */
public class SingletonExample2 {

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

根據Java Language Specification,JVM自己保證一個類在一個ClassLoader中只會被初始化一次。那麼根據classloader的這個機制,咱們在類裝載時就實例化,保證線程安全。

可是,有些時候,這種建立方法並不靈活。例如實例是依賴參數或者配置文件的,在getInstance()前必須調用某些方法設置它的參數。

 

5. 靜態內部類實現(static inner class):

/**
 * @author HKSCIDYX
 * static inner class: make it thread-safe and lazy-loading
 */
public class SingletonExample3 {

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

利用classloader保證線程安全。這種方法與第四種方法最大的區別是,就算SingletonExample3類被裝載了,instance不必定被初始化,由於holder類沒有被主動使用。相比而言,這種方法比第四種方法更加合理。

 

6. 枚舉實現(Enum):

《Effective Java, 2nd》第三條:enum是實現Singleton的最佳方法

/**
 * @author HKSCIDYX
 * Enum
 */
public enum SingletonExample4 {

    INSTANCE;
    
    public void whateverMethod(){
    
    }
    
}

這種作法,其實還沒真正在項目或者工做中見過。根據《Effective Java, 2nd》第三條,這種實現方法:

1. 簡潔

2. JVM能夠保證enum類的建立是線程安全(意味着其它方法的線程安全得由程序員本身去保證),

3. JVM能夠無償提供序列化機制。傳統的單例模式實現方法都有個問題:一旦實現了serializable接口,他們就再也不是單例的了。由於readObject()方法總會返回一個新的實例。所以爲了維護並保證單例,必須聲明全部實例域都是transient的,且提供一個readRevolve()方法:

/**
 * 
 * @author HKSCIDYX
 * Handle Serialized situation
 */
public class SingletonExample5 implements Serializable{

    private static final long serialVersionUID = 1L;
    
    private static SingletonExample5 INSTANCE = new SingletonExample5();
    
    //if there's other states to maintain, it must be transient
    
    private SingletonExample5(){}
    
    public static SingletonExample5 getInstance(){
      return INSTANCE;
    }
    
    private Object readResolve(){
      return INSTANCE;
    }
    
}

 

 總結

 1. 單例模式,並非整個程序或者整個應用只有一個實例,而是整個classloader只有一個實例。若是單例由不一樣的類裝載器裝入,那便有可能存在多個單例類的實例。假定不是遠端存取,例如一些servlet容器對每一個servlet使用徹底不一樣的類裝載器,這樣的話若是有兩個servlet訪問一個單例類,它們就都會有各自的實例

2. 單例模式,會使測試、找錯變得困難(根據《Effective Java, 2nd,第三條》) ,嘗試使用DI框架(Juice/Spring)來管理。

3. 什麼狀況下單例模式會失效(JPMorgan)?

Serialization, Reflection, multiple ClassLoader, multiple JVM, broken doubled checked locking(JDK4 or below) etc

參考:

《Effective Java, 2nd》

《設計模式解析,2nd》

http://icyfenix.iteye.com/blog/575052

http://xuze.me/blog/2013/01/31/singleton-pattern-seven-written/

http://837062099.iteye.com/blog/1454934

http://javarevisited.blogspot.hk/2011/03/10-interview-questions-on-singleton.html

相關文章
相關標籤/搜索