在我們日常的工作中經常需要在應用程序中保持一個唯一的實例,如:IO處理,數據庫操作等,由於這些對象都要占用重要的系統資源,所以我們必須限制這些實例的創建或始終使用一個公用的實例,這就是我們今天要介紹的——單例模式(Singleton)。
單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。
這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
注意:
我們知道,一個類的對象的產生是由類構造函數來完成的。如果一個類對外提供了public
的構造方法,那麼外界就可以任意創建該類的對象。所以,如果想限制對象的產生,一個辦法就是將構造函數變為私有的(至少是受保護的),使外面的類不能通過引用來產生對象。同時為了保證類的可用性,就必須提供一個自己的對象以及訪問這個對象的靜態方法。
實現單例模式的思路是:一個類能返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱);當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用;同時我們還將該類的構造函數定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例。
我們將創建一個 SingleObject 類。SingleObject 類有它的私有構造函數和本身的一個靜態實例。
SingleObject 類提供了一個靜態方法,供外界獲取它的靜態實例。SingletonPatternDemo,我們的演示類使用 SingleObject 類來獲取SingleObject 對象。
①創建一個 Singleton 類。
SingleObject.java
public class SingleObject { //創建 SingleObject 的一個對象 private static SingleObject instance = new SingleObject(); //讓構造函數為 private,這樣該類就不會被實例化 private SingleObject(){} //獲取唯一可用的對象 public static SingleObject getInstance(){ return instance; } public void showMessage(){ System.out.println("Hello World!"); } }
②從 singleton 類獲取唯一的對象。
SingletonPatternDemo.java
public class SingletonPatternDemo { public static void main(String[] args) { //不合法的構造函數 //編譯時錯誤:構造函數 SingleObject() 是不可見的 //SingleObject object = new SingleObject(); //獲取唯一可用的對象 SingleObject object = SingleObject.getInstance(); //顯示消息 object.showMessage(); } }
輸出結果:
Hello World!
描述:這種方式是最基本的實現方式,這種實現最大的問題就是不支持多線程。因為沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
這段代碼簡單明了,而且使用了懶加載( lazy loading )模式,但是卻存在致命的問題。當有多個線程並行調用 getInstance() 的時候,就會創建多個實例。也就是說在多線程下不能正常工作。
接下來介紹的幾種實現方式都支持多線程,但是在性能上有所差異。
為了解決上面的問題,最簡單的方法是將整個 getInstance() 方法設為同步(synchronized)。
描述:這種方式具備很好的 lazy loading,能夠在多線程中很好的工作,但是,效率很低,99% 情況下不需要同步。
優點:第一次調用才初始化,避免內存浪費。
缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。
getInstance() 的性能對應用程序不是很關鍵(該方法使用不太頻繁)。
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
雖然做到了線程安全,並且解決了多實例的問題,但是它並不高效。因為在任何時候只能有一個線程調用 getInstance() 方法。但是同步操作只需要在第一次調用時才被需要(即if語句中判斷 instance 為null時才調用,不為null時是直接返回instance的),即第一次創建單例實例對象時。這就引出了雙重檢驗鎖。
雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,因為會有兩次檢查 instance == null
,一次是在同步塊外,一次是在同步塊內。為什麼在同步塊內還要再檢驗一次?因為可能會有多個線程一起進入同步塊外的 if,如果在同步塊內不進行二次檢驗的話就會生成多個實例了。
描述:這種方式采用雙鎖機制,安全且在多線程情況下能保持高性能。
getInstance() 的性能對應用程序很關鍵。
public class Singleton { private volatile static Singleton singleton;//聲明成 volatile private Singleton() {} public static Singleton getSingleton() { if (instance == null) { // Single Checked synchronized (Singleton.class) { if (instance == null) { // Double Checked instance = new Singleton(); } } } return instance; } }
注意上面使用了volatile 關鍵字。
如果去掉volatile 關鍵字,它是有問題。主要在於instance = new Singleton()
這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。
所以我們只需要將 instance 變量聲明成 volatile 就可以了。
有些人認為使用 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。
相信你不會喜歡這種復雜又隱含問題的方式,當然我們有更好的實現線程安全的單例模式的辦法。
餓漢法就是在第一次引用該類的時候就創建對象實例,而不管實際是否需要創建。
描述:這種方式比較常用,但容易產生垃圾對象。
優點:沒有加鎖,執行效率會提高。
缺點:類加載時就初始化,浪費內存。
它基於 classloder 機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化 instance 顯然沒有達到 lazy loading 的效果。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
這種寫法如果完美的話,就沒必要在啰嗦那麼多雙檢鎖的問題了。缺點是它不是一種懶加載模式(lazy initialization),單例會在加載類後一開始就被初始化,即使客戶端沒有調用 getInstance()方法。餓漢式的創建方式在一些場景中將無法使用:譬如 Singleton 實例的創建是依賴參數或者配置文件的,在 getInstance() 之前必須調用某個方法設置參數給它,那樣這種單例寫法就無法使用了。
描述:這種方式能達到雙檢鎖方式一樣的功效,但實現更簡單。對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用於靜態域的情況,雙檢鎖方式可在實例域需要延遲初始化時使用。
這種方式同樣利用了 classloder 機制來保證初始化 instance 時只有一個線程,它跟第 4 種方式不同的是:第 4 種方式只要 Singleton 類被裝載了,那麼 instance 就會被實例化(沒有達到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不一定被初始化。因為 SingletonHolder 類沒有被主動使用,只有顯示通過調用 getInstance 方法時,才會顯示裝載 SingletonHolder 類,從而實例化 instance。想象一下,如果實例化 instance 很消耗資源,所以想讓它延遲加載,另外一方面,又不希望在 Singleton 類加載時就實例化,因為不能確保 Singleton 類還可能在其他的地方被主動使用從而被加載,那麼這個時候實例化 instance 顯然是不合適的。這個時候,這種方式相比第 4 種方式就顯得很合理。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
這種寫法仍然使用JVM本身機制保證了線程安全問題;由於 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。
描述:這種實現方式還沒有被廣泛采用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止多次實例化。
這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化。不過,由於 JDK1.5 之後才加入 enum 特性,用這種方式寫不免讓人感覺生疏,在實際工作中,也很少用。
不能通過 reflection attack 來調用私有構造方法。
public enum Singleton { INSTANCE; public void whateverMethod() { } }
單例的特點:外界無法通過構造器來創建對象,該類必須提供一個靜態方法向外界提供該類的唯一實例。
實現一個單例有兩點注意事項,①將構造器私有,不允許外界通過構造器創建對象;②通過公開的靜態方法向外界返回類的唯一實例。
一般來說,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、枚舉。上述所說都是線程安全的實現,文章開頭給出的第1種方法不算正確的寫法。
經驗之談:一般情況下,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 4種餓漢方式。只有在要明確實現 lazy loading 效果時,才會使用第 5 種登記方式。如果涉及到反序列化創建對象時,可以嘗試使用第 6 種枚舉方式。如果有其他特殊的需求,可以考慮使用第 3 種雙檢鎖方式。
相關文檔鏈接:
1、單例模式
2、如何正確地寫出單例模式
3、你真的會寫單例模式嗎——Java實現
4、設計模式(二)——單例模式