Java 单例模式探讨
以下是我再次研究單例(Java?單例模式缺點)時在網上收集的資料,相信你們看完就對單例完全掌握了
Java單例模式應該是看起來以及用起來簡單的一種設計模式,但是就實現方式以及原理來說,也并不淺顯哦。
?
總結一下我所知道的單例模式實現方式:
?
1.預先加載法
?
Java代碼??
優點:??
1.線程安全的
2.在類加載的同時已經創建好一個靜態對象,調用時反應速度快。
缺點:?資源利用效率不高,可能getInstance永遠不會執行到,但是執行了該類的其他靜態方法或者加載了該類(class.forName),那么這個實例仍然初始化了
?
2.initialization on demand,延遲加載法? (考慮多線程)
?
Java代碼??
優點:?資源利用率高,不執行getInstance就不會被實例,可以執行該類其他靜態方法。
缺點:?第一次加載時發應不快? ,多線程使用不必要的同步開銷大
?
3.initialization on demand double check 雙重檢測(?考慮多線程?)
?
Java代碼??
優點:?資源利用率高,?不執行getInstance就不會被實例,可以執行該類其他靜態方法。
缺點:?第一次加載時發應不快? ,由于java 內存模型一些原因偶爾會失敗
?
4.initialization on demand holder? (考慮多線程)
?
Java代碼??
優點:?資源利用率高,?不執行getInstance就不會被實例,可以執行該類其他靜態方法。
缺點:?第一次加載時發應不快
?
?
?
總結:?一般采用 1 即可,若對資源十分在意也可考慮 4 ,不要使用2,3了。
?
?
測試代碼:(暫不探討Class.forName類加載機制)
?
Java代碼作為對象的創建模式[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 引擎:一個作為內網給公司的網站管理人員使用;另一個給公司的外部客戶使用。兩者共享同一個數據庫,兩個系統都需要調用同一個單例類。如果這個單例類是有狀態的單例類的話,那么內網和外網用戶看到的單例對象的狀態就會不同。除非系統有協調機制,不然在這種情況下應當盡量避免使用有狀態的單例類。
from:?http://blog.csdn.net/it_man/article/details/5787567
總結
以上是生活随笔為你收集整理的Java 单例模式探讨的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java常见异常说明汇总
- 下一篇: java美元兑换,(Java实现) 美元