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


題外話:好久沒寫blog,做知識歸納整理了。本來設計模式就是個坑,各種文章也寫爛了。不過,不是自己寫的東西,缺少點知識的存在感。目前還沒做到光看即能記住,得寫。所以准備跳入設計模式這個大坑。

 

開篇先貢獻給

單例模式(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是可行的,取決於是否能保證2、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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM