前言
Java中單例(Singleton)模式是一種廣泛使用的設(shè)計(jì)模式。單例模式的主要作用是保證在Java程序中,某個(gè)類只有一個(gè)實(shí)例存在。一些管理器和控制器常被設(shè)計(jì)成單例模式。
單例模式有很多好處,它能夠避免實(shí)例對(duì)象的重復(fù)創(chuàng)建,不僅可以減少每次創(chuàng)建對(duì)象的時(shí)間開(kāi)銷,還可以節(jié)約內(nèi)存空間;能夠避免由于操作多個(gè)實(shí)例導(dǎo)致的邏輯錯(cuò)誤。如果一個(gè)對(duì)象有可能貫穿整個(gè)應(yīng)用程序,而且起到了全局統(tǒng)一管理控制的作用,那么單例模式也許是一個(gè)值得考慮的選擇。
1、單例模式UML類圖

2、單例模式的八種寫(xiě)法
2.1餓漢模式
顧名思義,餓漢法就是在第一次引用該類的時(shí)候就創(chuàng)建對(duì)象實(shí)例,而不管實(shí)際是否需要?jiǎng)?chuàng)建。代碼如下:
public class Singleton {
private static Singleton = new Singleton();
private Singleton() {}
public static getSignleton(){
return singleton;
}
}
這樣做的好處是編寫(xiě)簡(jiǎn)單,但是無(wú)法做到延遲創(chuàng)建對(duì)象。但是我們很多時(shí)候都希望對(duì)象可以盡可能地延遲加載,從而減小負(fù)載,所以就需要下面的懶漢法。
2.2 餓漢模式變種
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}
表面上看起來(lái)差別挺大,其實(shí)上面那種差不多,都是在類初始化即實(shí)例化instance。.
2.3不加鎖懶漢模式(線程不安全)
懶漢模式中單例是在需要的時(shí)候才去創(chuàng)建的,如果單例已經(jīng)創(chuàng)建,再次調(diào)用獲取接口將不會(huì)重新創(chuàng)建新的對(duì)象,而是直接返回之前創(chuàng)建的對(duì)象。如果某個(gè)單例使用的次數(shù)少,并且創(chuàng)建單例消耗的資源較多,那么就需要實(shí)現(xiàn)單例的按需創(chuàng)建,這個(gè)時(shí)候使用懶漢模式就是一個(gè)不錯(cuò)的選擇。但是這里的懶漢模式并沒(méi)有考慮線程安全問(wèn)題,在多個(gè)線程可能會(huì)并發(fā)調(diào)用它的getInstance()方法,就有很大可能導(dǎo)致重復(fù)創(chuàng)建對(duì)象。
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null) singleton = new Singleton();
return singleton;
}
}
2.4加鎖懶漢模式(線程安全,但是耗時(shí))
這種寫(xiě)法考慮了線程安全,將對(duì)singleton的null判斷以及new的部分使用synchronized進(jìn)行加鎖。同時(shí),對(duì)singleton對(duì)象使用volatile關(guān)鍵字進(jìn)行限制,保證其對(duì)所有線程的可見(jiàn)性,并且禁止對(duì)其進(jìn)行指令重排序優(yōu)化。如此即可從語(yǔ)義上保證這種單例模式寫(xiě)法是線程安全的。但是每次通過(guò)getInstance方法得到singleton實(shí)例的時(shí)候都有一個(gè)試圖去獲取同步鎖的過(guò)程。而眾所周知,加鎖是很耗時(shí)的,對(duì)高并發(fā)操作很不友好。
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
2.5雙重校驗(yàn)鎖( 兼顧線程安全和效率的寫(xiě)法)
雖然上面這種寫(xiě)法是可以正確運(yùn)行的,但是其效率低下,還是無(wú)法實(shí)際應(yīng)用。因?yàn)槊看握{(diào)用getSingleton()方法,都必須在synchronized這里進(jìn)行排隊(duì),而真正遇到需要new的情況是非常少的。所以,就誕生了第三種寫(xiě)法:
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
這種寫(xiě)法被稱為“雙重檢查鎖”,顧名思義,就是在getSingleton()方法中,進(jìn)行兩次null檢查??此贫啻艘慌e,但實(shí)際上卻極大提升了并發(fā)度,進(jìn)而提升了性能。為什么可以提高并發(fā)度呢?就像上文說(shuō)的,在單例中new的情況非常少,絕大多數(shù)都是可以并行的讀操作。因此在加鎖前多進(jìn)行一次null檢查就可以減少絕大多數(shù)的加鎖操作,執(zhí)行效率提高的目的也就達(dá)到了。
該種寫(xiě)法存在Java低版本中的問(wèn)題

那么,這種寫(xiě)法是不是絕對(duì)安全呢?前面說(shuō)了,從語(yǔ)義角度來(lái)看,并沒(méi)有什么問(wèn)題。但是其實(shí)還是有坑。說(shuō)這個(gè)坑之前我們要先來(lái)看看volatile這個(gè)關(guān)鍵字。其實(shí)這個(gè)關(guān)鍵字有兩層語(yǔ)義。第一層語(yǔ)義相信大家都比較熟悉,就是可見(jiàn)性??梢?jiàn)性指的是在一個(gè)線程中對(duì)該變量的修改會(huì)馬上由工作內(nèi)存(Work Memory)寫(xiě)回主內(nèi)存(Main Memory),所以會(huì)馬上反應(yīng)在其它線程的讀取操作中。順便一提,工作內(nèi)存和主內(nèi)存可以近似理解為實(shí)際電腦中的高速緩存和主存,工作內(nèi)存是線程獨(dú)享的,主存是線程共享的。volatile的第二層語(yǔ)義是禁止指令重排序優(yōu)化。大家知道我們寫(xiě)的代碼(尤其是多線程代碼),由于編譯器優(yōu)化,在實(shí)際執(zhí)行的時(shí)候可能與我們編寫(xiě)的順序不同。編譯器只保證程序執(zhí)行結(jié)果與源代碼相同,卻不保證實(shí)際指令的順序與源代碼相同。這在單線程看起來(lái)沒(méi)什么問(wèn)題,然而一旦引入多線程,這種亂序就可能導(dǎo)致嚴(yán)重問(wèn)題。volatile關(guān)鍵字就可以從語(yǔ)義上解決這個(gè)問(wèn)題。
例如,考慮下面的事件序列:
- 線程A發(fā)現(xiàn)變量沒(méi)有被初始化, 然后它獲取鎖并開(kāi)始變量的初始化。
- 由于某些編程語(yǔ)言的語(yǔ)義,編譯器生成的代碼允許在線程A執(zhí)行完變量的初始化之前,更新變量并將其指向部分初始化的對(duì)象。
- 線程B發(fā)現(xiàn)共享變量已經(jīng)被初始化,并返回變量。由于線程B確信變量已被初始化,它沒(méi)有獲取鎖。如果在A完成初始化之前共享變量對(duì)B可見(jiàn)(這是由于A沒(méi)有完成初始化或者因?yàn)橐恍┏跏蓟闹颠€沒(méi)有穿過(guò)B使用的內(nèi)存(緩存一致性)),程序很可能會(huì)崩潰。
Symantec JIT 編譯 singletons[i].reference = new Singleton(); 這段代碼時(shí),如果不加volatile關(guān)鍵詞,會(huì)生成如下字節(jié)碼:
0206106A mov eax,0F97E78h
0206106F call 01F6B210 ; allocate space for
; Singleton, return result in eax
02061074 mov dword ptr [ebp],eax ; EBP is &singletons[i].reference
; store the unconstructed object here.
02061077 mov ecx,dword ptr [eax] ; dereference the handle to
; get the raw pointer
02061079 mov dword ptr [ecx],100h ; Next 4 lines are
0206107F mov dword ptr [ecx+4],200h ; Singleton's inlined constructor
02061086 mov dword ptr [ecx+8],400h
0206108D mov dword ptr [ecx+0Ch],0F84030h
可以看到,在執(zhí)行Singleton的構(gòu)造函數(shù)之前,Singleton的新實(shí)例就被賦值給了singletons[i].reference,這在Java內(nèi)存模型中是完全合法的。
注意,前面反復(fù)提到“從語(yǔ)義上講是沒(méi)有問(wèn)題的”,但是很不幸,禁止指令重排優(yōu)化這條語(yǔ)義直到j(luò)dk1.5以后才能正確工作。此前的JDK中即使將變量聲明為volatile也無(wú)法完全避免重排序所導(dǎo)致的問(wèn)題。所以,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無(wú)法保證線程安全的。
2.6 靜態(tài)內(nèi)部類法(推薦)
那么,有沒(méi)有一種延時(shí)加載,并且能保證線程安全的簡(jiǎn)單寫(xiě)法呢?我們可以把Singleton實(shí)例放到一個(gè)靜態(tài)內(nèi)部類中,這樣就避免了靜態(tài)實(shí)例在Singleton類加載的時(shí)候就創(chuàng)建對(duì)象,并且由于靜態(tài)內(nèi)部類只會(huì)被加載一次,所以這種寫(xiě)法也是線程安全的:
public class Singleton {
private static class Holder {
private static Singleton singleton = new Singleton();
}
private Singleton(){}
public static Singleton getSingleton(){
return Holder.singleton;
}
}
但是,上面提到的所有實(shí)現(xiàn)方式都有兩個(gè)共同的缺點(diǎn):
- 都需要額外的工作(Serializable、transient、readResolve())來(lái)實(shí)現(xiàn)序列化,否則每次反序列化一個(gè)序列化的對(duì)象實(shí)例時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例。
- 可能會(huì)有人使用反射強(qiáng)行調(diào)用我們的私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器,讓它在創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋異常)。
2.7 枚舉寫(xiě)法
當(dāng)然,還有一種更加優(yōu)雅的方法來(lái)實(shí)現(xiàn)單例模式,那就是枚舉寫(xiě)法:
public class Resource{
}
public enum SomeThing {
INSTANCE;
private Resource instance;
SomeThing() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
調(diào)用
Resource resource = SomeThing.INSTANCE.getInstance();
使用枚舉除了線程安全和防止反射強(qiáng)行調(diào)用構(gòu)造器之外,還提供了自動(dòng)序列化機(jī)制,防止反序列化的時(shí)候創(chuàng)建新的對(duì)象。因此,Effective Java推薦盡可能地使用枚舉來(lái)實(shí)現(xiàn)單例。
2.8 容器實(shí)現(xiàn)單例模式
import java.util.HashMap;
import java.util.Map;
public class Singleton {
private static Map<String, Object> objMap = new HashMap<String, Object>();
private Singleton() {
}
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
這種實(shí)現(xiàn)方式使得我們可以管理多種類型的單例,并且在使用時(shí)可以通過(guò)統(tǒng)一接口進(jìn)行獲取操作,降低用戶使用成本,也對(duì)用戶隱藏了具體實(shí)現(xiàn),降低耦合度。
3 、單例模式在Android源碼中應(yīng)用
第三方 ImageLoader(通過(guò)源碼分析,得到單例模式中雙重檢測(cè)方案)
LayoutInflater 單例模式通過(guò)容器進(jìn)行管理
LayoutInflater 源碼分析 WindowManager、ActivityManager、PowerManager都是容器管理
總結(jié)
代碼沒(méi)有一勞永逸的寫(xiě)法,只有在特定條件下最合適的寫(xiě)法。在不同的平臺(tái)、不同的開(kāi)發(fā)環(huán)境(尤其是jdk版本)下,自然有不同的最優(yōu)解(或者說(shuō)較優(yōu)解)。
比如枚舉,雖然Effective Java中推薦使用,但是在Android平臺(tái)上卻是不被推薦的。在這篇Android Training中明確指出:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
再比如雙重檢查鎖法,不能在jdk1.5之前使用,而在Android平臺(tái)上使用就比較放心了(一般Android都是jdk1.6以上了,不僅修正了volatile的語(yǔ)義問(wèn)題,還加入了不少鎖優(yōu)化,使得多線程同步的開(kāi)銷降低不少)。
最后,不管采取何種方案,請(qǐng)時(shí)刻牢記單例的三大要點(diǎn):
- 線程安全
- 延遲加載
- 序列化與反序列化安全