單例模式(Singleton)
單例對(duì)象(Singleton)是一種常用的設(shè)計(jì)模式。在Java應(yīng)用中,單例對(duì)象能保證在一個(gè)JVM中,該對(duì)象只有一個(gè)實(shí)例存在。
這樣的模式有幾個(gè)好處:
某些類(lèi)創(chuàng)建比較頻繁,對(duì)于一些大型的對(duì)象,這是一筆很大的系統(tǒng)開(kāi)銷(xiāo)。
省去了new操作符,降低了系統(tǒng)內(nèi)存的使用頻率,減輕GC壓力。
有些類(lèi)如交易所的核心交易引擎,控制著交易流程,如果該類(lèi)可以創(chuàng)建多個(gè)的話(huà),系統(tǒng)完全亂了。(比如一個(gè)軍隊(duì)出現(xiàn)了多個(gè)司令員同時(shí)指揮,肯定會(huì)亂成一團(tuán)),所以只有使用單例模式,才能保證核心交易服務(wù)器獨(dú)立控制整個(gè)流程。
首先我們寫(xiě)一個(gè)簡(jiǎn)單的單例類(lèi):
public class Singleton {
/* 持有私有靜態(tài)實(shí)例,防止被引用,此處賦值為null,目的是實(shí)現(xiàn)延遲加載 */
private static Singleton instance = null;
/* 私有構(gòu)造方法,防止被實(shí)例化 */
private Singleton() {
}
/* 靜態(tài)工程方法,創(chuàng)建實(shí)例 */
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/* 如果該對(duì)象被用于序列化,可以保證對(duì)象在序列化前后保持一致 */
public Object readResolve() {
return instance;
}
}
這個(gè)類(lèi)可以滿(mǎn)足基本要求,但是,像這樣毫無(wú)線(xiàn)程安全保護(hù)的類(lèi),如果我們把它放入多線(xiàn)程的環(huán)境下,肯定就會(huì)出現(xiàn)問(wèn)題了,如何解決?
我們首先會(huì)想到對(duì)getInstance方法加synchronized關(guān)鍵字,如下:
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
但是,synchronized關(guān)鍵字鎖住的是這個(gè)對(duì)象,這樣的用法,在性能上會(huì)有所下降,因?yàn)槊看握{(diào)用getInstance(),都要對(duì)對(duì)象上鎖,事實(shí)上,只有在第一次創(chuàng)建對(duì)象的時(shí)候需要加鎖,之后就不需要了,所以,這個(gè)地方需要改進(jìn)。我們改成下面這個(gè):
public static Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
似乎解決了之前提到的問(wèn)題,將synchronized關(guān)鍵字加在了內(nèi)部,也就是說(shuō)當(dāng)調(diào)用的時(shí)候是不需要加鎖的,只有在instance為null,并創(chuàng)建對(duì)象的時(shí)候才需要加鎖,性能有一定的提升。
但是,這樣的情況,還是有可能有問(wèn)題的,看下面的情況:
在Java指令中創(chuàng)建對(duì)象和賦值操作是分開(kāi)進(jìn)行的,也就是說(shuō)instance = new Singleton();語(yǔ)句是分兩步執(zhí)行的。但是JVM并不保證這兩個(gè)操作的先后順序,也就是說(shuō)有可能JVM會(huì)為新的Singleton實(shí)例分配空間,然后直接賦值給instance成員,然后再去初始化這個(gè)Singleton實(shí)例。這樣就可能出錯(cuò)了,我們以A、B兩個(gè)線(xiàn)程為例:
A、B線(xiàn)程同時(shí)進(jìn)入了第一個(gè)if判斷
A首先進(jìn)入synchronized塊,由于instance為null,所以它執(zhí)行instance = new Singleton();
由于JVM內(nèi)部的優(yōu)化機(jī)制,JVM先畫(huà)出了一些分配給Singleton實(shí)例的空白內(nèi)存,并賦值給instance成員(注意此時(shí)JVM沒(méi)有開(kāi)始初始化這個(gè)實(shí)例),然后A離開(kāi)了synchronized塊。
B進(jìn)入synchronized塊,由于instance此時(shí)不是null,因此它馬上離開(kāi)了synchronized塊并將結(jié)果返回給調(diào)用該方法的程序。
此時(shí)B線(xiàn)程打算使用Singleton實(shí)例,卻發(fā)現(xiàn)它沒(méi)有被初始化,于是錯(cuò)誤發(fā)生了。
所以程序還是有可能發(fā)生錯(cuò)誤,其實(shí)程序在運(yùn)行過(guò)程是很復(fù)雜的,從這點(diǎn)我們就可以看出,尤其是在寫(xiě)多線(xiàn)程環(huán)境下的程序更有難度,有挑戰(zhàn)性。
我們對(duì)該程序做進(jìn)一步優(yōu)化:
private static class SingletonFactory{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonFactory.instance;
}
實(shí)際情況是,單例模式使用內(nèi)部類(lèi)來(lái)維護(hù)單例的實(shí)現(xiàn),JVM內(nèi)部的機(jī)制能夠保證當(dāng)一個(gè)類(lèi)被加載的時(shí)候,這個(gè)類(lèi)的加載過(guò)程是線(xiàn)程互斥的。這樣當(dāng)我們第一次調(diào)用getInstance的時(shí)候,JVM能夠幫我們保證instance只被創(chuàng)建一次,并且會(huì)保證把賦值給instance的內(nèi)存初始化完畢,這樣我們就不用擔(dān)心上面的問(wèn)題。同時(shí)該方法也只會(huì)在第一次調(diào)用的時(shí)候使用互斥機(jī)制,這樣就解決了低性能問(wèn)題。
這樣我們暫時(shí)總結(jié)一個(gè)完美的單例模式:
public class Singleton {
/* 私有構(gòu)造方法,防止被實(shí)例化 */
private Singleton() {
}
/* 此處使用一個(gè)內(nèi)部類(lèi)來(lái)維護(hù)單例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}
/* 獲取實(shí)例 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}
/* 如果該對(duì)象被用于序列化,可以保證對(duì)象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}
}
其實(shí)說(shuō)它完美,也不一定,如果在構(gòu)造函數(shù)中拋出異常,實(shí)例將永遠(yuǎn)得不到創(chuàng)建,也會(huì)出錯(cuò)。所以說(shuō),十分完美的東西是沒(méi)有的,我們只能根據(jù)實(shí)際情況,選擇最適合自己應(yīng)用場(chǎng)景的實(shí)現(xiàn)方法。也有人這樣實(shí)現(xiàn):因?yàn)槲覀冎恍枰趧?chuàng)建類(lèi)的時(shí)候進(jìn)行同步,所以只要將創(chuàng)建和getInstance()分開(kāi),單獨(dú)為創(chuàng)建加synchronized關(guān)鍵字,也是可以的:
public class SingletonTest {
private static SingletonTest instance = null;
private SingletonTest() {
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new SingletonTest();
}
}
public static SingletonTest getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
}
考慮性能的話(huà),整個(gè)程序只需創(chuàng)建一次實(shí)例,所以性能也不會(huì)有什么影響。
補(bǔ)充:采用"影子實(shí)例"的辦法為單例對(duì)象的屬性同步更新
public class SingletonTest {
private static SingletonTest instance = null;
private Vector properties = null;
public Vector getProperties() {
return properties;
}
private SingletonTest() {
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new SingletonTest();
}
}
public static SingletonTest getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
public void updateProperties() {
SingletonTest shadow = new SingletonTest();
properties = shadow.getProperties();
}
}
通過(guò)單例模式的學(xué)習(xí)告訴我們:
單例模式理解起來(lái)簡(jiǎn)單,但是具體實(shí)現(xiàn)起來(lái)還是有一定的難度。
synchronized關(guān)鍵字鎖定的是對(duì)象,在用的時(shí)候,一定要在恰當(dāng)?shù)牡胤绞褂茫ㄗ⒁庑枰褂面i的對(duì)象和過(guò)程,可能有的時(shí)候并不是整個(gè)對(duì)象及整個(gè)過(guò)程都需要鎖)。
到這兒,單例模式基本已經(jīng)講完了,結(jié)尾處,筆者突然想到另一個(gè)問(wèn)題,就是采用類(lèi)的靜態(tài)方法,實(shí)現(xiàn)單例模式的效果,也是可行的,此處二者有什么不同?
首先,靜態(tài)類(lèi)不能實(shí)現(xiàn)接口。(從類(lèi)的角度說(shuō)是可以的,但是那樣就破壞了靜態(tài)了。因?yàn)榻涌谥胁辉试S有static修飾的方法,所以即使實(shí)現(xiàn)了也是非靜態(tài)的)
其次,單例可以被延遲初始化,靜態(tài)類(lèi)一般在第一次加載是初始化。之所以延遲加載,是因?yàn)橛行╊?lèi)比較龐大,所以延遲加載有助于提升性能。
再次,單例類(lèi)可以被繼承,他的方法可以被覆寫(xiě)。但是靜態(tài)類(lèi)內(nèi)部方法都是static,無(wú)法被覆寫(xiě)。
最后一點(diǎn),單例類(lèi)比較靈活,畢竟從實(shí)現(xiàn)上只是一個(gè)普通的Java類(lèi),只要滿(mǎn)足單例的基本需求,你可以在里面隨心所欲的實(shí)現(xiàn)一些其它功能,但是靜態(tài)類(lèi)不行。
從上面這些概括中,基本可以看出二者的區(qū)別,但是,從另一方面講,我們上面最后實(shí)現(xiàn)的那個(gè)單例模式,內(nèi)部就是用一個(gè)靜態(tài)類(lèi)來(lái)實(shí)現(xiàn)的,所以,二者有很大的關(guān)聯(lián),只是我們考慮問(wèn)題的層面不同罷了。兩種思想的結(jié)合,才能造就出完美的解決方案,就像HashMap采用數(shù)組+鏈表來(lái)實(shí)現(xiàn)一樣,其實(shí)生活中很多事情都是這樣,單用不同的方法來(lái)處理問(wèn)題,總是有優(yōu)點(diǎn)也有缺點(diǎn),最完美的方法是,結(jié)合各個(gè)方法的優(yōu)點(diǎn),才能最好的解決問(wèn)題!