5種實現(xiàn)單例模式的方式
單例模式
單例模式(Singleton),目的是為了保證在一個進程中,某個類有且僅有一個實例。
由于這個類只有一個實例,所以不能讓調(diào)用方使用new Xxx()
來創(chuàng)建實例。所以,單例的構(gòu)造方法必須是private
,這樣就防止了調(diào)用方自己創(chuàng)建實例。
單例模式的實現(xiàn)需要三個必要的條件:
單例類的構(gòu)造函數(shù)必須是私有的,這樣才能將類的創(chuàng)建權(quán)控制在類的內(nèi)部,從而使得類的外部不能創(chuàng)建類的實例。
單例類通過一個私有的靜態(tài)變量來存儲其唯一實例。
單例類通過提供一個公開的靜態(tài)方法,使得外部使用者可以訪問類的唯一實例。
另外,實現(xiàn)單例類時,還需要考慮三個問題:
創(chuàng)建單例對象時,是否線程安全。
單例對象的創(chuàng)建,是否延時加載。
獲取單例對象時,是否需要加鎖。
下面介紹幾種實現(xiàn)單例模式的方式。
餓漢模式
JVM在類的初始化階段,會執(zhí)行類的靜態(tài)方法。在執(zhí)行類的初始化期間,JVM會去獲取Class對象的鎖。這個鎖可以同步多個線程對同一個類的初始化。
餓漢模式只在類加載的時候創(chuàng)建一次實例,沒有多線程同步的問題。單例沒有用到也會被創(chuàng)建,而且在類加載之后就被創(chuàng)建,內(nèi)存就被浪費了。
public?class?Singleton?{??
????private?static?Singleton?instance?=?new?Singleton();??
????private?Singleton()?{}??
????public?static?Singleton?newInstance()?{
????????return?instance;??
????}??
}
餓漢式單例的優(yōu)點:
單例對象的創(chuàng)建是線程安全的;
獲取單例對象時不需要加鎖。
餓漢式單例的缺點:單例對象的創(chuàng)建,不是延時加載。
懶漢式
與餓漢式思想不同,懶漢式支持延時加載,將對象的創(chuàng)建延遲到了獲取對象的時候。不過為了線程安全,在獲取對象的操作需要加鎖,這就導(dǎo)致了低性能。
public?class?Singleton?{?
??private?static?final?Singleton?instance;
??
??private?Singleton?()?{}
??
??public?static?synchronized?Singleton?getInstance()?{????
????if?(instance?==?null)?{??????
??????instance?=?new?Singleton();????
????}????
????return?instance;??
??}
}
上述代碼加的鎖只有在第一次創(chuàng)建對象時有用,而之后每次獲取對象,其實是不需要加鎖的(雙重檢查鎖定優(yōu)化了這個問題)。
懶漢式單例優(yōu)點:
對象的創(chuàng)建是線程安全的。
支持延時加載。
懶漢式單例缺點:
獲取對象的操作被加上了鎖,影響了并發(fā)性能。
雙重檢查鎖定
雙重檢查鎖定將懶漢式中的 synchronized
方法改成了 synchronized
代碼塊。如下:
public?class?Singleton?{??
????private?static?volatile?Singleton?instance?=?null;??//volatile
????private?Singleton(){}??
????public?static?Singleton?getInstance()?{??
????????if?(instance?==?null)?{??
????????????synchronized?(Singleton.class)?{??
????????????????if?(instance?==?null)?{
????????????????????instance?=?new?Singleton();??
????????????????}??
????????????}??
????????}??
????????return?instance;??
????}??
}??
雙重校驗鎖先判斷 instance 是否已經(jīng)被實例化,如果沒有被實例化,那么才對實例化語句進行加鎖。
instance使用static修飾的原因:getInstance為靜態(tài)方法,因為靜態(tài)方法的內(nèi)部不能直接使用非靜態(tài)變量,只有靜態(tài)成員才能在沒有創(chuàng)建對象時進行初始化,所以返回的這個實例必須是靜態(tài)的。
為什么兩次判斷instance == null
:
TimeThread AThread BT1檢查到instance
為空
T2
檢查到instance
為空T3
初始化對象A
T4
返回對象A
T5初始化對象B
T6返回對象B
new Singleton()
會執(zhí)行三個動作:分配內(nèi)存空間、初始化對象和對象引用指向內(nèi)存地址。
memory?=?allocate();??//?1:分配對象的內(nèi)存空間
ctorInstance(memory);??//?2:初始化對象
instance?=?memory;????//?3:設(shè)置instance指向剛分配的內(nèi)存地址
由于指令重排優(yōu)化的存在,導(dǎo)致初始化對象和將對象引用指向內(nèi)存地址的順序是不確定的。在某個線程創(chuàng)建單例對象時,會為該對象分配了內(nèi)存空間并將對象的字段設(shè)置為默認(rèn)值。此時就可以將分配的內(nèi)存地址賦值給instance字段了,然而該對象可能還沒有初始化。若緊接著另外一個線程來調(diào)用getInstance,取到的是未初始化的對象,程序就會出錯。volatile 可以禁止指令重排序,保證了先初始化對象再賦值給instance變量。
TimeThread AThread BT1檢查到instance
為空
T2獲取鎖
T3再次檢查到instance
為空
T4為instance
分配內(nèi)存空間
T5將instance
指向內(nèi)存空間
T6
檢查到instance
不為空T7
訪問instance
(此時對象還未完成初始化)T8初始化instance
雙重檢查鎖定單例優(yōu)點:
對象的創(chuàng)建是線程安全的。
支持延時加載。
獲取對象時不需要加鎖。
靜態(tài)內(nèi)部類
它與餓漢模式一樣,也是利用了類初始化機制,因此不存在多線程并發(fā)的問題。不一樣的是,它是在內(nèi)部類里面去創(chuàng)建對象實例。這樣的話,只要應(yīng)用中不使用內(nèi)部類,JVM就不會去加載這個單例類,也就不會創(chuàng)建單例對象,從而實現(xiàn)懶漢式的延遲加載。也就是說這種方式可以同時保證延遲加載和線程安全。
基于類初始化的方案的實現(xiàn)代碼更簡潔。
public?class?Instance?{
????private?static?class?InstanceHolder?{
????????public?static?Instance?instance?=?new?Instance();
????}
????private?Instance()?{}
????public?static?Instance?getInstance()?{
????????return?InstanceHolder.instance?;??//?這里將導(dǎo)致InstanceHolder類被初始化
????}
}
如上述代碼,InstanceHolder
是一個靜態(tài)內(nèi)部類,當(dāng)外部類 Instance
被加載的時候,并不會創(chuàng)建 InstanceHolder
實例對象。
只有當(dāng)調(diào)用 getInstance()
方法時,InstanceHolder
才會被加載,這個時候才會創(chuàng)建 Instance
。Instance
的唯一性、創(chuàng)建過程的線程安全性,都由 JVM 來保證。
靜態(tài)內(nèi)部類單例優(yōu)點:
對象的創(chuàng)建是線程安全的。
支持延時加載。
獲取對象時不需要加鎖。
枚舉
用枚舉來實現(xiàn)單例,是最簡單的方式。這種實現(xiàn)方式通過 Java 枚舉類型本身的特性,保證了實例創(chuàng)建的線程安全性和實例的唯一性。
public?enum?Singleton?{
??INSTANCE;?//?該對象全局唯一
}