總結一下我所知道的單例模式實現方式:java
1.預先加載法web
class S1 { private S1() { System.out.println("ok1"); } private static S1 instance = new S1(); public static S1 getInstance() { return instance; } }
[java] view plaincopy安全
class S1 { private S1() { System.out.println("ok1"); } private static S1 instance = new S1(); public static S1 getInstance() { return instance; } }
1.線程安全的服務器
2.在類加載的同時已經建立好一個靜態對象,調用時反應速度快。
缺點: 資源利用效率不高,可能getInstance永遠不會執行到,可是執行了該類的其餘靜態方法或者加載了該類(class.forName),那麼這個實例仍然初始化了多線程
2.initialization on demand,延遲加載法 (考慮多線程)app
class S2 { private S2() { System.out.println("ok2"); } private static S2 instance = null; public static synchronized S2 getInstance() { if (instance == null) instance = new S2(); return instance; } }
[java] view plaincopy函數
class S2 { private S2() { System.out.println("ok2"); } private static S2 instance = null; public static synchronized S2 getInstance() { if (instance == null) instance = new S2(); return instance; } }
優勢: 資源利用率高,不執行getInstance就不會被實例,能夠執行該類其餘靜態方法。工具
缺點: 第一次加載時發應不快 ,多線程使用沒必要要的同步開銷大
3.initialization on demand double check 雙重檢測( 考慮多線程 )
Java代碼 class S3 { private S3() { System.out.println("ok3"); } private static S3 instance = null; public static S3 getInstance() { if (instance == null) { synchronized (S3.class) { if (instance == null) instance = new S3(); } } return instance; } } class S3 { private S3() { System.out.println("ok3"); } private static S3 instance = null; public static S3 getInstance() { if (instance == null) { synchronized (S3.class) { if (instance == null) instance = new S3(); } } return instance; } }
優勢: 資源利用率高, 不執行getInstance就不會被實例,能夠執行該類其餘靜態方法。
缺點: 第一次加載時發應不快 ,因爲java 內存模型一些緣由偶爾會失敗
4.initialization on demand holder (考慮多線程)
Java代碼 class S4 { private S4() { System.out.println("ok4"); } private static class S4Holder { static S4 instance = new S4(); } public static S4 getInstance() { return S4Holder.instance; } }
[java] view plaincopy
class S4 { private S4() { System.out.println("ok4"); } private static class S4Holder { static S4 instance = new S4(); } public static S4 getInstance() { return S4Holder.instance; } }
優勢: 資源利用率高, 不執行getInstance就不會被實例,能夠執行該類其餘靜態方法。
缺點: 第一次加載時發應不快
總結: 通常採用 1 便可,若對資源十分在乎也可考慮 4 ,不要使用2,3了。
測試代碼:(暫不探討Class.forName類加載機制)
/**
* Created by IntelliJ IDEA.
* User: yiminghe
* Date: 2009-6-8
* Time: 19:20:52
*/
public class Singleton {
public static void main(String[] args) throws Exception{
System.out.println(Class.forName("S1"));
System.out.println(Class.forName("S2"));
System.out.println(Class.forName("S3"));
System.out.println(Class.forName("S4"));
}
}
/*
預先加載法
優勢:1.線程安全的,
2.在類加載的同時已經建立好一個靜態對象,調用時反應速度快。
缺點: 資源利用效率不高,可能這個單例不會須要使用也被系統加載
*/
class S1 { private S1() { System.out.println("ok1"); } private static S1 instance = new S1(); public static S1 getInstance() { return instance; } }
/* initialization on demand,延遲加載法 (考慮多線程) 優勢:1.資源利用率高 缺點:第一次加載是發應不快 ,多線程使用沒必要要的同步開銷大 */ class S2 { private S2() { System.out.println("ok2"); } private static S2 instance = null; public static synchronized S2 getInstance() { if (instance == null) instance = new S2(); return instance; } }
/
/*
* initialization on demand - double check 延遲加載法改進之雙重檢測 (考慮多線程) 優勢:1.資源利用率高 缺點:第一次加載是發應不快 ,因爲java 內存模型一些緣由偶爾會失敗 */ class S3 { private S3() { System.out.println("ok3"); } private static S3 instance = null; public static S3 getInstance() { if (instance == null) { synchronized (S3.class) { if (instance == null) instance = new S3(); } } return instance; } }
initialization on demand holder (考慮多線程) 優勢:1.資源利用率高 缺點:第一次加載是發應不快 */ class S4 { private S4() { System.out.println("ok4"); } private static class S4Holder { static S4 instance = new S4(); } public static S4 getInstance() { return S4Holder.instance; } }
做爲對象的建立模式[GOF95], 單例模式確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例。這個類稱爲單例類。 注:本文乃閻宏博士的《Java與模式》一書的第十五章。 引言 單例模式的要點 單例單例 顯然單例模式的要點有三個;一是某各種只能有一個實例;二是它必須自行建立這個事例;三是它必須自行向整個系統提供這個實例。在下面的對象圖中,有一個"單例對象",而"客戶甲"、"客戶乙" 和"客戶丙"是單例對象的三個客戶對象。能夠看到,全部的客戶對象共享一個單例對象。並且從單例對象到自身的鏈接線能夠看出,單例對象持有對本身的引用。 資源管理 一些資源管理器經常設計成單例模式。 在計算機系統中,須要管理的資源包括軟件外部資源,譬如每臺計算機能夠有若干個打印機,但只能有一個Printer Spooler, 以免兩個打印做業同時輸出到打印機中。每臺計算機能夠有若干傳真卡,可是隻應該有一個軟件負責管理傳真卡,以免出現兩份傳真做業同時傳到傳真卡中的狀況。每臺計算機能夠有若干通訊端口,系統應當集中管理這些通訊端口,以免一個通訊端口同時被兩個請求同時調用。 須要管理的資源包括軟件內部資源,譬如,大多數的軟件都有一個(甚至多個)屬性(properties)文件存放系統配置。這樣的系統應當由一個對象來管理一個屬性文件。 須要管理的軟件內部資源也包括譬如負責記錄網站來訪人數的部件,記錄軟件系統內部事件、出錯信息的部件,或是對系統的表現進行檢查的部件等。這些部件都必須集中管理,不可政出多頭。 這些資源管理器構件必須只有一個實例,這是其一;它們必須自行初始化,這是其二;容許整個系統訪問本身這是其三。所以,它們都知足單例模式的條件,是單例模式的應用。 一個例子:Windows 回收站 Windows 9x 之後的視窗系統中都有一個回收站,下圖就顯示了Windows 2000 的回收站。 在整個視窗系統中,回收站只能有一個實例,整個系統都使用這個唯一的實例,並且回收站自行提供本身的實例。所以,回收站是單例模式的應用。 雙重檢查成例 在本章最後的附錄裏研究了雙重檢查成例。雙重檢查成例與單例模式並沒有直接的關係,可是因爲不少C 語言設計師在單例模式裏面使用雙重檢查成例,因此這一作法也被不少Java 設計師所模仿。所以,本書在附錄裏提醒讀者,雙重檢查成例在Java 語言裏並不能成立,詳情請見本章的附錄。 單例模式的結構 單例模式有如下的特色: .. 單例類只可有一個實例。 .. 單例類必須本身建立本身這唯一的實例。 .. 單例類必須給全部其餘對象提供這一實例。 雖然單例模式中的單例類被限定只能有一個實例,可是單例模式和單例類能夠很容易被推廣到任意且有限多個實例的狀況,這時候稱它爲多例模式(Multiton Pattern) 和多例類(Multiton Class),請見"專題:多例(Multiton )模式與多語言支持"一章。單例類的簡略類圖以下所示。 因爲Java 語言的特色,使得單例模式在Java 語言的實現上有本身的特色。這些特色主要表如今單例類如何將本身實例化上。 餓漢式單例類餓漢式單例類是在Java 語言裏實現得最爲簡便的單例類,下面所示的類圖描述了一個餓漢式單例類的典型實現。 從圖中能夠看出,此類已經自已將本身實例化。 代碼清單1:餓漢式單例類 public class EagerSingleton { private static final EagerSingleton m_instance = new EagerSingleton(); /** * 私有的默認構造子 */ private EagerSingleton() { } /** * 靜態工廠方法 */ public static EagerSingleton getInstance() { ·224·Java 與模式 return m_instance; } } 讀者能夠看出,在這個類被加載時,靜態變量m_instance 會被初始化,此時類的私有構造子會被調用。這時候,單例類的唯一實例就被建立出來了。 Java 語言中單例類的一個最重要的特色是類的構造子是私有的,從而避免外界利用構造子直接建立出任意多的實例。值得指出的是,因爲構造子是私有的,所以,此類不能被繼承。 懶漢式單例類 與餓漢式單例類相同之處是,類的構造子是私有的。與餓漢式單例類不一樣的是,懶漢式單例類在第一次被引用時將本身實例化。若是加載器是靜態的,那麼在懶漢式單例類被加載時不會將本身實例化。以下圖所示,類圖中給出了一個典型的餓漢式單例類實現。 代碼清單2:懶漢式單例類 package com.javapatterns.singleton.demos; public class LazySingleton { private static LazySingleton m_instance = null; /** * 私有的默認構造子,保證外界沒法直接實例化 */ private LazySingleton() { } /** * 靜態工廠方法,返還此類的唯一實例 */ synchronized public static LazySingleton getInstance() { if (m_instance == null) { m_instance = new LazySingleton(); } return m_instance; } } 讀者可能會注意到,在上面給出懶漢式單例類實現裏對靜態工廠方法使用了同步化,以處理多線程環境。有些設計師在這裏建議使用所謂的"雙重檢查成例"。必須指出的是,"雙重檢查成例"不能夠在Java 語言中使用。不十分熟悉的讀者,能夠看看後面給出的小節。 一樣,因爲構造子是私有的,所以,此類不能被繼承。餓漢式單例類在本身被加載時就將本身實例化。即使加載器是靜態的,在餓漢式單例類被加載時仍會將本身實例化。單從資源利用效率角度來說,這個比懶漢式單例類稍差些。 從速度和反應時間角度來說,則比懶漢式單例類稍好些。然而,懶漢式單例類在實例化時, 必須處理好在多個線程同時首次引用此類時的訪問限制問題,特別是當單例類做爲資源控制器,在實例化時必然涉及資源初始化,而資源初始化頗有可能耗費時間。這意味着出現多線程同時首次引用此類的機率變得較大。 餓漢式單例類能夠在Java 語言內實現, 但不易在C++ 內實現,由於靜態初始化在C++ 裏沒有固定的順序,於是靜態的m_instance 變量的初始化與類的加載順序沒有保證,可能會出問題。這就是爲何GoF 在提出單例類的概念時,舉的例子是懶漢式的。他們的書影響之大,以至Java 語言中單例類的例子也大可能是懶漢式的。實際上,本書認爲餓漢式單例類更符合Java 語言自己的特色。 登記式單例類 登記式單例類是GoF 爲了克服餓漢式單例類及懶漢式單例類均不可繼承的缺點而設計的。本書把他們的例子翻譯爲Java 語言,並將它本身實例化的方式從懶漢式改成餓漢式。只是它的子類實例化的方式只能是懶漢式的, 這是沒法改變的。以下圖所示是登記式單例類的一個例子,圖中的關係線代表,此類已將本身實例化。 代碼清單3:登記式單例類 import java.util.HashMap; public class RegSingleton { static private HashMap m_registry = new HashMap(); static { RegSingleton x = new RegSingleton(); m_registry.put( x.getClass().getName() , x); } /** * 保護的默認構造子 */ protected RegSingleton() {} /** * 靜態工廠方法,返還此類唯一的實例 */ static public RegSingleton getInstance(String name) { if (name == null) { name = "com.javapatterns.singleton.demos.RegSingleton"; } if (m_registry.get(name) == null) { try { m_registry.put( name, Class.forName(name).newInstance() ) ; } catch(Exception e) { System.out.println("Error happened."); } } return (RegSingleton) (m_registry.get(name) ); } /** * 一個示意性的商業方法 */ public String about() { return "Hello, I am RegSingleton."; } } 它的子類RegSingletonChild 須要父類的幫助才能實例化。下圖所示是登記式單例類子類的一個例子。圖中的關係代表,此類是由父類將子類實例化的。 下面是子類的源代碼。 代碼清單4:登記式單例類的子類 import java.util.HashMap; public class RegSingletonChild extends RegSingleton { public RegSingletonChild() {} /** * 靜態工廠方法 */ static public RegSingletonChild getInstance() { return (RegSingletonChild) RegSingleton.getInstance( "com.javapatterns.singleton.demos.RegSingletonChild" ); } /** * 一個示意性的商業方法 */ public String about() { return "Hello, I am RegSingletonChild."; } } 在GoF 原始的例子中,並無getInstance() 方法,這樣獲得子類必須調用的getInstance(String name)方法並傳入子類的名字,所以很不方便。本章在登記式單例類子類的例子裏,加入了getInstance() 方法,這樣作的好處是RegSingletonChild 能夠經過這個方法,返還自已的實例。而這樣作的缺點是,因爲數據類型不一樣,沒法在RegSingleton 提供這樣一個方法。因爲子類必須容許父類以構造子調用產生實例,所以,它的構造子必須是公開的。這樣一來,就等於容許了以這樣方式產生實例而不在父類的登記中。這是登記式單例類的一個缺點。 GoF 曾指出,因爲父類的實例必須存在纔可能有子類的實例,這在有些狀況下是一個浪費。這是登記式單例類的另外一個缺點。 |
GoF 曾指出,因爲父類的實例必須存在纔可能有子類的實例,這在有些狀況下是一個浪費。這是登記式單例類的另外一個缺點。
在什麼狀況下使用單例模式
使用單例模式的條件
使用單例模式有一個很重要的必要條件:
在一個系統要求一個類只有一個實例時才應當使用單例模式。反過來講,若是一個類能夠有幾個實例共存,那麼就沒有必要使用單例類。可是有經驗的讀者可能會看到不少不當地使用單例模式的例子,可見作到上面這一點並不容易,下面就是一些這樣的狀況。
例子一
問:個人一個系統須要一些"全程"變量。學習了單例模式後,我發現可使用一個單例類盛放全部的"全程"變量。請問這樣作對嗎?
答:這樣作是違背單例模式的用意的。單例模式只應當在有真正的"單一實例"的需求時纔可以使用。
一個設計得當的系統不該當有所謂的"全程"變量,這些變量應當放到它們所描述的實體所對應的類中去。將這些變量從它們所描述的實體類中抽出來, 放到一個不相干的單例類中去,會使得這些變量產生錯誤的依賴關係和耦合關係。
例子二
問:個人一個系統須要管理與數據庫的鏈接。學習了單例模式後,我發現可使用一個單例類包裝一個Connection 對象,並在finalize()方法中關閉這個Connection 對象。這樣的話,在這個單例類的實例沒有被人引用時,這個finalize()對象就會被調用,所以,Connection 對象就會被釋放。這多妙啊。
答:這樣作是不恰當的。除非有單一實例的需求,否則不要使用單例模式。在這裏Connection 對象能夠同時有幾個實例共存,不須要是單一實例。
單例模式有不少的錯誤使用案例都與此例子類似,它們都是試圖使用單例模式管理共享資源的生命週期,這是不恰當的。
單例類的狀態
有狀態的單例類
一個單例類能夠是有狀態的(stateful),一個有狀態的單例對象通常也是可變(mutable) 單例對象。
有狀態的可變的單例對象經常當作狀態庫(repositary)使用。好比一個單例對象能夠持有一個int 類型的屬性,用來給一個系統提供一個數值唯一的序列號碼,做爲某個販賣系統的帳單號碼。固然,一個單例類能夠持有一個彙集,從而容許存儲多個狀態。
沒有狀態的單例類
另外一方面,單例類也能夠是沒有狀態的(stateless),僅用作提供工具性函數的對象。既然是爲了提供工具性函數,也就沒有必要建立多個實例,所以使用單例模式很合適。一個沒有狀態的單例類也就是不變(Immutable) 單例類; 關於不變模式,讀者能夠參見本書的"不變(Immutable )模式"一章。
多個JVM 系統的分散式系統
EJB 容器有能力將一個EJB 的實例跨過幾個JVM 調用。因爲單例對象不是EJB,所以,單例類侷限於某一個JVM 中。換言之,若是EJB 在跨過JVM 後仍然須要引用同一個單例類的話,這個單例類就會在數個JVM 中被實例化,形成多個單例對象的實例出現。一個J2EE應用系統可能分佈在數個JVM 中,這時候不必定須要EJB 就能形成多個單例類的實例出如今不一樣JVM 中的狀況。
若是這個單例類是沒有狀態的,那麼就沒有問題。由於沒有狀態的對象是沒有區別的。可是若是這個單例類是有狀態的,那麼問題就來了。舉例來講,若是一個單例對象能夠持有一個int 類型的屬性,用來給一個系統提供一個數值唯一的序列號碼,做爲某個販賣系統的帳單號碼的話,用戶會看到同一個號碼出現好幾回。
在任何使用了EJB、RMI 和JINI 技術的分散式系統中,應當避免使用有狀態的單例模式。
多個類加載器
同一個JVM 中會有多個類加載器,當兩個類加載器同時加載同一個類時,會出現兩個實例。在不少J2EE 服務器容許同一個服務器內有幾個Servlet 引擎時,每個引擎都有獨立的類加載器,經有不一樣的類加載器加載的對象之間是絕緣的。
好比一個J2EE 系統所在的J2EE 服務器中有兩個Servlet 引擎:一個做爲內網給公司的網站管理人員使用;另外一個給公司的外部客戶使用。二者共享同一個數據庫,兩個系統都須要調用同一個單例類。若是這個單例類是有狀態的單例類的話,那麼內網和外網用戶看到的單例對象的狀態就會不一樣。除非系統有協調機制,否則在這種狀況下應當儘可能避免使用有狀態的單例類。