一、
引子
單例模式是設計模式中使用很頻繁的一種模式,在各種開源框架、應用系統中多有應用,在我前面的幾篇文章中也結合其它模式使用到了單例模式。這里我們就單例模式進行系統的學習。并對有人提出的
“
單例模式是邪惡的
”
這個觀點進行了一定的分析。
二、
定義與結構
單例模式又叫做單態模式或者單件模式。在
GOF
書中給出的定義為:保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。單例模式中的
“
單例
”
通常用來代表那些本質上具有唯一性的系統組件(或者叫做資源)。比如文件系統、資源管理器等等。
單
例模式的目的就是要控制特定的類只產生一個對象,當然也允許在一定情況下靈活的改變對象的個數。那么怎么來實現單例模式呢?一個類的對象的產生是由類構造
函數來完成的,如果想限制對象的產生,就要將構造函數變為私有的(至少是受保護的),使得外面的類不能通過引用來產生對象;同時為了保證類的可用性,就必
須提供一個自己的對象以及訪問這個對象的靜態方法。
現在對單例模式有了大概的了解了吧,其實單例模式在實現上是非常簡單的
——
只有一個角色,而客戶則通過調用類方法來得到類的對象。
放上一個類圖吧,這樣更直觀一些:
?
單例模式可分為有狀態的和無狀態的。有狀態的單例對象一般也是可變的單例對象,
多個單態對象在一起就可以作為一個狀態倉庫一樣向外提供服務。沒有狀態的單例對象也就是不變單例對象,僅用做提供工具函數。
三、
實現
在單例模式的實現上有幾種不同的方式,我在這里將一一講解。先來看一種方式,它在《
java
與模式》中被稱為餓漢式。
public class Singleton {
//
在自己內部定義自己一個實例
//
注意這是
private
只供內部調用
private static Singleton instance = new Singleton();
//
如上面所述,將構造函數設置為私有
private Singleton(){
}
//
靜態工廠方法,
提供了一個供外部訪問得到對象
的靜態方法
public static Singleton getInstance() {
return instance;
}
}
??????
下面這種方式被稱為懶漢式:
P
public class Singleton {
?????? //
和上面有什么不同?
private static Singleton instance = null;
//
設置為私有的構造函數
private Singleton(){
}
//
靜態工廠方法
public static synchronized Singleton getInstance() {
//
這個方法比上面有所改進
??????
if (instance==null)
??????
??????
instance
=
new Singleton();
??????
return instance;
}
}
先讓我們來比較一下這兩種實現方式。
首先他們的構造函數都是私有的,徹底斷開了使用構造函數來得到類的實例的通道,但是這樣也使得類失去了多態性(大概這就是為什么有人將這種模式稱作單態模式)。
?
在第二種方式中,對靜態工廠方法進行了同步處理,原因很明顯——為了防止多線程環境中產生多個實例;而在第一種方式中則不存在這種情況。
??????
在第二種方式中將類對自己的實例化延遲到第一次被引用的時候。而在第一種方式中則是在類被加載的時候實例化,這樣多次加載會照成多次實例化。但是第二種方式由于使用了同步處理,在反應速度上要比第一種慢一些。
??????
在
《
java
與模式》書中提到,就
java
語言來說,第一種方式更符合
java
語言本身的特點。
??????
以
上兩種實現方式均失去了多態性,不允許被繼承。還有另外一種靈活點的實現,將構造函數設置為受保護的,這樣允許被繼承產生子類。這種方式在具體實現上又有
所不同,可以將父類中獲得對象的靜態方法放到子類中再實現;也可以在父類的靜態方法中進行條件判斷來決定獲得哪一個對象;在
GOF
中認為最好的一種方式是維護一張存有對象和對應名稱的注冊表(可以使用
HashMap
來實現)。下面的實現參考《
java
與模式》采用帶有注冊表的方式。
import java.util.HashMap;
public class Singleton
{
//
用來存放對應關系
?????? private static HashMap sinRegistry = new HashMap();
?????? static private Singleton s = new Singleton();
?????? //
受保護的構造函數
?????? protected Singleton()
?????? {}
?????? public static Singleton getInstance(String name)
?????? {
????????????? if(name == null)
???????????????????? name = "Singleton";
????????????? if(sinRegistry.get(name)==null)
????????????? {
???????????????????? try{
??????????????????????????? sinRegistry.put(name , Class.forName(name).newInstance());
???????????????????? }catch(Exception e)
???????????????????? {
??????????????????????????? e.printStackTrace();
???????????????????? }?????
????????????? }
????????????? return (Singleton)(sinRegistry.get(name));?
?????? }
?????? public void test()
?????? {
????????????? System.out.println("getclasssuccess!");??????
?????? }
}
public class SingletonChild1 extends Singleton
{
?????? public SingletonChild1(){}
?????? static ?? public SingletonChild1 getInstance()
?????? {
????????????? return (SingletonChild1)Singleton.getInstance("SingletonChild1");?????
?????? }
?????? public void test()
?????? {
????????????? System.out.println("getclasssuccess111!");?
?????? }
}
在
java
中子類的構造函數的范圍不能比父類的小,所以可能存在不守規則的客戶程序使用其構造函數來產生實例。
四、單例模式邪惡論
看這題目也許有點夸張,不過這對初學者是一個很好的警告。單例模式在
java
中的使用存在很多陷阱和假象,這使得沒有意識到單例模式使用局限性的你在系統中布下了隱患……
其實這個問題早在
2001
年的時候就有人在網上系統的提出來過,我在這里只是老生常談了。但是對于大多的初學者來說,可能這樣的觀點在還很陌生。下面我就一一列舉出單例模式在
java
中存在的陷阱。
多個虛擬機
當系統中的單例類被拷貝運行在多個虛擬機下的時候,在每一個虛擬機下都可以創建一個實例對象。在使用了
EJB
、
JINI
、
RMI
技術的分布式系統中,由于中間件屏蔽掉了分布式系統在物理上的差異,所以對你來說,想知道具體哪個虛擬機下運行著哪個單例對象是很困難的。
因此,在使用以上分布技術的系統中,應該避免使用存在狀態的單例模式,因為一個有狀態的單例類,在不同虛擬機上,各個單例對象保存的狀態很可能是不一樣的,問題也就隨之產生。而且在
EJB
中不要使用單例模式來控制訪問資源,因為這是由
EJB
容器來負責的。在其它的分布式系統中,當每一個虛擬機中的資源是不同的時候,可以考慮使用單例模式來進行管理。
多個類加載器
當存在多個類加載器加載類的時候,即使它們加載的是相同包名,相同類名甚至每個字節都完全相同的類,也會被區別對待的。因為不同的類加載器會使用不同的命名空間(
namespace
)來區分同一個類。因此,單例類在多加載器的環境下會產生多個單例對象。
也許你認為出現多個類加載器的情況并不是很多。其實多個類加載器存在的情況并不少見。在很多
J2EE
服務器上允許存在多個
servlet
引擎,而每個引擎是采用不同的類加載器的;瀏覽器中
applet
小程序通過網絡加載類的時候,由于安全因素,采用的是特殊的類加載器,等等。
??????
這種情況下,由狀態的單例模式也會給系統帶來隱患。因此除非系統由協調機制,在一般情況下不要使用存在狀態的單例模式。
??????
錯誤的同步處理
??????
在使用上面介紹的懶漢式單例模式時,同步處理的恰當與否也是至關重要的。不然可能會達不到得到單個對象的效果,還可能引發死鎖等錯誤。因此在使用懶漢式單例模式時一定要對同步有所了解。不過使用餓漢式單例模式就可以避免這個問題。
??????
子類破壞了對象控制
??????
在上一節介紹最后一種擴展性較好的單例模式實現方式的時候,就提到,由于類構造函數變得不再私有,就有可能失去對對象的控制。這種情況只能通過良好的文檔來規范。
??????
串行化(可序列化)
為了使一個單例類變成可串行化的,僅僅在聲明中添加“
implements Serializable
”是不夠的。因為一個串行化的對象在每次返串行化的時候,都會創建一個新的對象,而不僅僅是一個對原有對象的引用。為了防止這種情況,可以在單例類中加入
readResolve
方法。
關于這個方法的具體情況請參考《
Effective Java
》一書第
57
條建議。
其實對象的串行化并不僅局限于上述方式,還存在基于
XML
格式的對象串行化方式。這種方式也存在上述的問題,所以在使用的時候要格外小心。
?????? 上面羅列了一些使用單例模式時可能會遇到的問題。而且這些問題都和 java 中的類、線程、虛擬機等基礎而又復雜的概念交織在一起,你如果稍不留神……。但是這并不代表著單例模式就一無是處,更不能一棒子將其打死。它還是不可缺少的一種基礎設計模式,它對一些問題提供了非常有效的解決方案,在 java 中你完全可以把它看成編碼規范來學習,只是使用的時候要考慮周全些就可以了。
五、 題外話
拋開單例模式,使用下面一種簡單的方式也能得到單例,而且如果你確信此類永遠是單例的,使用下面這種方式也許更好一些。
public static final Singleton INSTANCE = new Singleton();
而使用單例模式提供的方式,這可以在不改變
API
的情況下,改變我們對單例類的具體要求。
六、
總結