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