1. 程式人生 > >JAVA單例模式的各種寫法分析,最優為列舉

JAVA單例模式的各種寫法分析,最優為列舉

作用

單例模式(Singleton):保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點

適用場景

  1. 應用中某個例項物件需要頻繁的被訪問。

  2. 應用中每次啟動只會存在一個例項。如資料庫系統。

使用方式

  1. 懶漢式
public class Singleton {  
  
    /* 持有私有靜態例項,防止被引用,此處賦值為null,目的是實現延遲載入 */  
    private static Singleton instance = null;  
  
    /* 私有構造方法,防止被例項化 */  
    private Singleton() {  
    }  
  
    /* 懶漢式,靜態工程方法,建立例項 */
public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

呼叫:

Singleton.getInstance() ;

優點:延遲載入(需要的時候才去載入) 缺點: 執行緒不安全,在多執行緒中很容易出現不同步的情況,如在資料庫物件進行的頻繁讀寫操作時。

2.懶漢式變種,加同步鎖

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

更一般的寫法是這樣

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

這種寫法能夠在多執行緒中很好的工作,而且看起來它也具備很好的lazy loading,但是,遺憾的是,效率很低,99%情況下不需要同步。 在Android原始碼中使用的該單例方法有:InputMethodManager,AccessibilityManager等都是使用這種單例模式。

3.懶漢式變種,雙重檢驗鎖 (Double Check Locking) (DCL)

public class Singleton {    
  
   private static Singleton instance = null;    
     
  // 只在第一次初始化的時候加上同步鎖*/  
  public static Singleton getInstance() {  
      if (instance == null) {  
          synchronized (Singleton.class) {  
              if (instance == null) {  
                  instance = new Singleton();  
              }  
          }  
      }  
      return instance;  
  }  
}

這種方法貌似很完美的解決了上述效率的問題,它或許在併發量不多,安全性不太高的情況能完美執行,但是,這種方法也有不幸的地方。問題就是出現在這句

instance = new Singleton();

在JVM編譯的過程中會出現指令重排的優化過程,這就會導致當 instance實際上還沒初始化,就可能被分配了記憶體空間,也就是說會出現 instance !=null 但是又沒初始化的情況,這樣就會導致返回的 instance 不完整。

我們來看看這個場景:假設執行緒一執行到instance = new SingletonKerrigan()這句,這裡看起來是一句話,但實際上它並不是一個原子操作(原子操作的意思就是這條語句要麼就被執行完,要麼就沒有被執行過,不能出現執行了一半這種情形)。事實上高階語言裡面非原子操作有很多,我們只要看看這句話被編譯後在JVM執行的對應彙編程式碼就發現,這句話被編譯成8條彙編指令,大致做了3件事情: 1.給Kerrigan的例項分配記憶體。 2.初始化Kerrigan的構造器 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 SingletonKerriganD instance = null;”就可以保證每次都去instance都從主記憶體讀取,就可以使用DCL的寫法來完成單例模式。當然volatile或多或少也會影響到效能,最重要的是我們還要考慮JDK1.42以及之前的版本,所以本文中單例模式寫法的改進還在繼續。

在android影象開源專案Android-Universal-Image-Loader (https://github.com/nostra13/Android-Universal-Image-Loader) 中使用的是這種方式。

4、餓漢式

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

這種寫法不會出現併發問題,但是它是餓漢式的,在ClassLoader載入類後Kerrigan的例項就會第一時間被建立,餓漢式的建立方式在一些場景中將無法使用:譬如例項的建立是依賴引數或者配置檔案的,在getInstance()之前必須呼叫某個方法設定引數給它,那樣這種單例寫法就無法使用了。

5、靜態內部類

public class Singleton {  
 
    private static class SingletonHolder {  
        private static Singleton instance = new Singleton();  
    }  
  
    /** 
     * 私有的建構函式 
     */  
    private Singleton() {  
  
    }  
  
    public static Singleton getInstance() {  
        return SingletonHolder.instance;  
    } 
}

這種寫法仍然使用JVM本身機制保證了執行緒安全問題;由於SingletonHolder是私有的,除了getInstance()之外沒有辦法訪問它,因此它是懶漢式的;同時讀取例項的時候不會進行同步,沒有效能缺陷;也不依賴JDK版本。

這種方式是Singleton類被裝載了,instance不一定被初始化。因為SingletonHolder類沒有被主動使用,只有顯示通過呼叫getInstance方法時,才會顯示裝載SingletonHolder類,從而例項化instance。想象一下,如果例項化instance很消耗資源,我想讓他延遲載入,另外一方面,我不希望在Singleton類載入時就例項化,因為我不能確保Singleton類還可能在其他的地方被主動使用從而被載入,那麼這個時候例項化instance顯然是不合適的。這個時候,這種方式就顯得很合理。

6、列舉

public enum SingletonEnum {  
    /** 
     * 1.從Java1.5開始支援; 
     * 2.無償提供序列化機制; 
     * 3.絕對防止多次例項化,即使在面對複雜的序列化或者反射攻擊的時候; 
     */  
    instance;  
    private String others;  
    SingletonEnum() {  
  
       } 
    }

這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還能防止反序列化重新建立新的物件。

總結

建立單例例項的方式有: 1.直接new單例物件 2.通過反射構造單例物件 3.通過序列化構造單例物件。

對於第一種情況,一般我們會加入一個private或者protected的建構函式,這樣系統就不會自動新增那個public的構造函數了,因此只能呼叫裡面的static方法,無法通過new建立物件。

對於第二種情況,如果單例由不同的類裝載器裝入,那便有可能存在多個單例類的例項。假定不是遠端存取,例如一些servlet容器對每個servlet使用完全不同的類 裝載器,這樣的話如果有兩個servlet訪問一個單例類,它們就都會有各自的例項。修復的辦法是:

private static Class getClass(String classname)      
                                         throws ClassNotFoundException {     
       ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     
        
      if(classLoader == null)     
           classLoader = Singleton.class.getClassLoader();     
       
      return (classLoader.loadClass(classname));     
    }     
}

對於第三種情況,如果單例物件有必要實現Serializable介面(很少出現),則應當同時實現readResolve()方法來保證反序列化的時候得到原來的物件。寫法如下:

public class Singleton implements Serializable {    
    private static class SingletonHolder {    
       /**  
         * 單例物件例項  
         */    
        static final Singleton INSTANCE = new Singleton();    
    }    
     
    public static Singleton getInstance() {    
       return SingletonHolder.INSTANCE;    
    }    
   /**  
     * private的建構函式用於避免外界直接使用new來例項化物件  
     */    
    private Singleton() {    
    }    
     
    /**  
    * readResolve方法應對單例物件被序列化時候  
     */    
    private Object readResolve() {    
        return getInstance();    
   }    
}