1. 單例模式的實(shí)現(xiàn)方式
1.1. 餓漢模式
// 最簡單的單例實(shí)現(xiàn)
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
// 餓漢模式的一種變種
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
上面兩種實(shí)現(xiàn)方式的思想其實(shí)是一樣的陪拘,就是在類加載的時(shí)候?qū)嵗粋€(gè)對象茬底,這樣避免了線程安全的問題(關(guān)于線程安全問題在下面的例子中會提到)饿肺,但是這也可能會造成一些不必要的資源消耗,比如僅僅載入了類,這時(shí)候?qū)ο笠呀?jīng)實(shí)例化了匿又,但是有可能我們永遠(yuǎn)都用不到它。
1.2. 懶漢模式
public class Singleton {
private static Singleton instance;
private Singleton() {
}
// 同步方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懶漢模式的整體思想是當(dāng)需要一個(gè)單例對象的時(shí)候建蹄,判斷對象是否已經(jīng)實(shí)例化碌更,如果沒有,則進(jìn)行實(shí)例化洞慎,否則直接返回已經(jīng)實(shí)例化的對象痛单。這里用了「同步方法」,這樣可以保證「線程安全」劲腿,當(dāng)一個(gè)線程調(diào)用該方法時(shí)旭绒,另一個(gè)方法正好也要調(diào)用該方法,則需要等待前一個(gè)線程調(diào)用完畢,這樣就可以保證在多線程的情況下挥吵,對象也只會被實(shí)例化一次重父。
但是這種方式在性能上會有一些缺點(diǎn),因?yàn)槊看握{(diào)用 getInstance
方法都需要進(jìn)行同步忽匈,會造成不必要的同步開銷房午。
1.3 Double Check Lock (DCL)模式
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
「雙重校驗(yàn)鎖」模式的重點(diǎn)就是雙重校驗(yàn),它通過兩次 instance == null
的判斷保證了線程安全丹允,同時(shí)避免了上一種「懶漢模式」的同步性能消耗郭厌。
我們一步一步進(jìn)行分析,假設(shè)現(xiàn)在有三個(gè)線程 A嫌松、B 和 C 都要使用單例對象沪曙,它們都去調(diào)用 getInstance
方法。
- 線程 A 先走到外層 if 處萎羔,判斷發(fā)現(xiàn) instance 還未實(shí)例化液走,此時(shí)就會進(jìn)入同步代碼塊。
- 與此同時(shí)贾陷,線程 B 也走到了外層 if 語句處缘眶,判斷發(fā)現(xiàn) instance 仍未實(shí)例化,進(jìn)行下一步髓废,但是由于此時(shí)線程 A 已經(jīng)獲得了同步鎖巷懈,正在執(zhí)行同步塊中的代碼,線程 B 需要等待慌洪。
- 線程 A 進(jìn)行內(nèi)層 if 判斷顶燕,發(fā)現(xiàn) instance 為 null,還沒有實(shí)例化冈爹,則對 instance 進(jìn)行實(shí)例化涌攻,然后退出同步塊,獲得 instance 對象频伤。
- 由于線程 A 退出同步塊恳谎,線程 B 獲得同步鎖,進(jìn)入同步塊憋肖,執(zhí)行其中的代碼因痛,進(jìn)行內(nèi)層 if 判斷,發(fā)現(xiàn) instance 對象已經(jīng)被實(shí)例化岸更,于是退出同步塊鸵膏,獲得 instance 對象。
- 線程 C 調(diào)用
getInstance
怎炊,進(jìn)行外層 if 判斷较性,發(fā)現(xiàn) instance 已經(jīng)被實(shí)例化用僧,直接獲得 instance 對象,不再需要進(jìn)行同步操作赞咙。
通過分析责循,我們可以得出 instance 兩次判空的用意,內(nèi)層判空是為了在 instance 還未實(shí)例化的時(shí)候創(chuàng)建實(shí)例攀操,而外層判斷是為了在 instance 已經(jīng)實(shí)例化的時(shí)候避免不必要的同步操作院仿。
然而,這種模式并不是最優(yōu)的模式速和,依然存在一些問題歹垫,我們繼續(xù)進(jìn)行分析。
首先我們需要知道颠放, instance = new Singleton()
這句代碼看似是一句代碼排惨,但是它最終會被編譯成多條匯編指令,大概做了下面 3 件事情:
- 為 Singleton 實(shí)例分配內(nèi)存碰凶。
- 調(diào)用構(gòu)造函數(shù)暮芭,初始化成員字段。
- 將 instance 對象引用指向分配的內(nèi)存空間欲低,經(jīng)歷這一步之后辕宏,instance 才不為 null。
但是由于 Java 編譯器允許處理器亂序處理砾莱,也就是說瑞筐,步驟 2 和步驟 3 的順序是可以交換的,我們假設(shè)一種情況腊瑟,當(dāng)一個(gè)線程先執(zhí)行了步驟 1 和 3聚假,但是步驟 2 還沒有執(zhí)行,此時(shí) instance 已經(jīng)不為 null 了闰非,但成員字段還沒有完成初始化膘格,如果此時(shí)切換到另一個(gè)線程,在外層 if 判空的時(shí)候就會得到非空的結(jié)果河胎,從而獲得成員字段未初始化的 instance 對象闯袒,就會導(dǎo)致錯誤虎敦。
除了上面這一點(diǎn)游岳,還有一點(diǎn)就是在 JDK1.5 之前因?yàn)?Java 內(nèi)存模型中的 Cache、寄存器導(dǎo)主內(nèi)存會寫順序的規(guī)定其徙,也會導(dǎo)致這種 instance 非 null胚迫,但是成員字段未正確初始化的情況。
因此從 JDK1.5 開始唾那,官方調(diào)整了 JVM访锻,新增一個(gè) volatile 關(guān)鍵詞褪尝,只要在 instance 的定義前加上 volatile 關(guān)鍵字,也就是修改為 private volatile static Singleton instance;
就可以保證不會出現(xiàn)上述問題期犬。
1.4 靜態(tài)內(nèi)部類模式
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
}
這種方式是比較推薦的單例模式的實(shí)現(xiàn)方式河哑。這種方式其實(shí)是對餓漢式的一種升級,它很好地利用了內(nèi)部類來避免僅僅載入類龟虎,對象就已經(jīng)實(shí)例化的問題璃谨。只有在真正需要 instance 對象,調(diào)用 getInstance
方法的時(shí)候鲤妥,才會實(shí)例化對象佳吞。
1.5 枚舉單例
public enum SingletonEnum {
INSTANCE;
private int i;
public void fun() {
}
}
最簡單的單例模式實(shí)現(xiàn)方式就是使用枚舉了,其中的 INSTANCE 并沒有實(shí)際用途棉安,是因?yàn)楸仨氁幸粋€(gè)枚舉項(xiàng)底扳。枚舉在 Java 中和普通類是一樣的,可以有成員字段贡耽,也可以有成員方法衷模,而且使用枚舉不需要寫任何額外的代碼來保證線程安全。
之前提到的單例模式實(shí)現(xiàn)方法在一種特殊情況下仍會重復(fù)實(shí)例化對象菇爪,那就是在反序列化操作的時(shí)候算芯,為了避免出現(xiàn)這種情況,我們還需要在代碼中加入下面方法:
private Object readResolve() throws ObjectStreamException {
return instance;
}
而通過枚舉實(shí)現(xiàn)凳宙,這些都不需要考慮熙揍,即使是反序列化也不會重復(fù)實(shí)例化對象。
1.6 使用容器
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<>();
private SingletonManager() {
}
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);
}
}
使用容器來統(tǒng)一管理單例氏涩。