單例模式作為日常開(kāi)發(fā)中最常用的設(shè)計(jì)模式之一,是最基礎(chǔ)的設(shè)計(jì)模式,也是最需要熟練掌握的設(shè)計(jì)模式。單例模式的定義是:保證一個(gè)類(lèi)僅有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)。那么你知道單例模式有多少種實(shí)現(xiàn)方式嗎?以及每種實(shí)現(xiàn)方式的利弊呢?
餓漢模式代碼如下: public class Singleton { private static Singleton instance = new Singleton(); private Singleton () { } public static Singleton getInstance() { return instance; } } 這種方式在類(lèi)加載時(shí)就完成了實(shí)例化,會(huì)影響類(lèi)的加載速度,但獲取對(duì)象的速度快。 這種方式基于類(lèi)加載機(jī)制保證實(shí)例僅有一個(gè),避免了多線程的同步問(wèn)題,是線程安全的。? 懶漢模式(線程不安全)絕大多數(shù)時(shí)候,類(lèi)加載的時(shí)機(jī)和對(duì)象使用的時(shí)機(jī)都是分開(kāi)的,所以沒(méi)有必要在類(lèi)加載的時(shí)候就去實(shí)例化單例對(duì)象。為了消除單例對(duì)象實(shí)例化對(duì)類(lèi)加載的影響,引入了延遲加載,就有了懶漢模式的實(shí)現(xiàn)方式。代碼如下: public class Singleton { private static Singleton instance; private Singleton () {} public static Singleton getInstance() { if (instance == null) {instance = new Singleton(); } return instance; } } 懶漢模式聲明了一個(gè)靜態(tài)對(duì)象,在用戶(hù)第一次調(diào)用時(shí)完成實(shí)例化,屬于延遲加載方式。而且這種方式不是線程安全。? 懶漢模式(線程安全)針對(duì)線程不安全的懶漢模式,對(duì)其中的獲取單例對(duì)象的方法增加同步關(guān)鍵字。代碼如下: public class Singleton { private static Singleton instance; private Singleton () { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } 這種寫(xiě)法保證了線程安全,但是每次調(diào)用getInstance方法獲取單例時(shí)都需要進(jìn)行同步,造成不必要的同步開(kāi)銷(xiāo),但實(shí)際上除了第一次實(shí)例化需要同步,其他時(shí)候都是不需要同步的。 雙重檢查模式(DCL)既然懶漢模式中的實(shí)例化只需要在第一次的時(shí)候保證同步,那何不只在實(shí)例為空的時(shí)候加同步關(guān)鍵字呢。代碼如下: public class Singleton { private volatile static Singleton singleton; // 1 private Singleton () { } public static Singleton getInstance() { if (instance== null) { // 2 synchronized (Singleton.class) { // 3 if (instance== null) { // 4 instance= new Singleton(); // 5 } } } return singleton; } } 雙重檢查寫(xiě)法,主要關(guān)注以上代碼中的5點(diǎn):
DCL思考在以上代碼的第一步中,我們提到volatile關(guān)鍵字,volatile關(guān)鍵字除了保證內(nèi)存可見(jiàn)性,還有一點(diǎn)是禁止指令重排序。那么問(wèn)題出在哪里呢?對(duì),第5步。實(shí)際上,實(shí)例化對(duì)象的動(dòng)作并不是一個(gè)原子操作, memory = allocate(); // 5.1:分配對(duì)象的內(nèi)存空間ctorInstance(memory); // 5.2:初始化對(duì)象instance = memory; // 5.3: 設(shè)置instance指向剛分配的內(nèi)存地址 而上面三行代碼,5.2和5.3可能發(fā)生重排序。跟著上面代碼中的第二次檢查的位置進(jìn)行分析。當(dāng)線程B執(zhí)行到5.3之后,5.2之前時(shí),這時(shí)候線程A首次判斷單例對(duì)象是否為空。這時(shí)候當(dāng)然單例對(duì)象是不為空的,但是卻不能使用,因?yàn)閱卫龑?duì)象還沒(méi)有被初始化呢。這既是DCL的缺陷所在,也是為什么要對(duì)單例對(duì)象家volatile關(guān)鍵字的原因。禁止了指令重排序,自然不會(huì)出現(xiàn)線程A拿到一個(gè)不可用的單例對(duì)象。 靜態(tài)內(nèi)部類(lèi)單例模式public class Singleton { private Singleton() {}public static Singleton getInstance() { return SingletonHolder.sInstance; } private static class SingletonHolder { private static final Singleton sInstance = new Singleton(); } } 第一次加載Singleton類(lèi)時(shí)并不會(huì)初始化sInstance,只有第一次調(diào)用getInstance方法時(shí)虛擬機(jī)加載SingletonHolder 并初始化sInstance ,這樣不僅能確保線程安全也能保證Singleton類(lèi)的唯一性,所以推薦使用靜態(tài)內(nèi)部類(lèi)單例模式。 枚舉類(lèi)單例模式public enum Singleton { INSTANCE; public void doSomeThing() { } } 那這個(gè)單例如何來(lái)填充屬性呢,增加構(gòu)造函數(shù)和屬性即可啦,請(qǐng)看代碼: public enum Singleton { INSTANCE("name", 18);private String name;private int age;Singleton(String name, int age) {this.name = name;this.age = age;} public void doSomeThing() { } } 默認(rèn)枚舉實(shí)例的創(chuàng)建是線程安全的,并且在任何情況下都是單例,上述講的幾種單例模式實(shí)現(xiàn)中,有一種情況下他們會(huì)重新創(chuàng)建對(duì)象,那就是反序列化,將一個(gè)單例實(shí)例對(duì)象寫(xiě)到磁盤(pán)再讀回來(lái),從而獲得了一個(gè)實(shí)例。反序列化操作提供了readResolve方法,這個(gè)方法可以讓開(kāi)發(fā)人員控制對(duì)象的反序列化。在上述的幾個(gè)方法示例中如果要杜絕單例對(duì)象被反序列化是重新生成對(duì)象,就必須加入如下方法: private Object readResolve() throws ObjectStreamException{return singleton;} 使用容器實(shí)現(xiàn)單例模式代碼如下: public class SingletonManager { private static Map<String, Object> objMap = new HashMap<String,Object>(); private Singleton() { } public static void registerService(String key, Objectinstance) { if (!objMap.containsKey(key) ) { objMap.put(key, instance) ; } } public static ObjectgetService(String key) { return objMap.get(key) ; }} 在程序的初始化,將多個(gè)單例類(lèi)型注入到一個(gè)統(tǒng)一管理的類(lèi)中,使用時(shí)通過(guò)key來(lái)獲取對(duì)應(yīng)類(lèi)型的對(duì)象,這種方式使得我們可以管理多種類(lèi)型的單例,并且在使用時(shí)可以通過(guò)統(tǒng)一的接口進(jìn)行操作。這種方式是利用了Map的key唯一性來(lái)保證單例。 CAS實(shí)現(xiàn)單例模式以上實(shí)現(xiàn)主要用到了兩點(diǎn)來(lái)保證單例,一是JVM的類(lèi)加載機(jī)制,另一個(gè)就是加鎖了。那么有沒(méi)有不加鎖的線程安全的單例實(shí)現(xiàn)嗎?有點(diǎn),那就是使用CAS。CAS是項(xiàng)樂(lè)觀鎖技術(shù),當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí),只有其中一個(gè)線程能更新變量的值,而其它線程都失敗,失敗的線程并不會(huì)被掛起,而是被告知這次競(jìng)爭(zhēng)中失敗,并可以再次嘗試。代碼如下: public class Singleton {private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();private Singleton() {}public static Singleton getInstance() {for (;;) {Singleton singleton = INSTANCE.get();if (null != singleton) {return singleton;}singleton = new Singleton();if (INSTANCE.compareAndSet(null, singleton)) {return singleton;}}}} 用CAS的好處在于不需要使用傳統(tǒng)的鎖機(jī)制來(lái)保證線程安全,CAS是一種基于忙等待的算法,依賴(lài)底層硬件的實(shí)現(xiàn),相對(duì)于鎖它沒(méi)有線程切換和阻塞的額外消耗,可以支持較大的并行度。CAS的一個(gè)重要缺點(diǎn)在于如果忙等待一直執(zhí)行不成功(一直在死循環(huán)中),會(huì)對(duì)CPU造成較大的執(zhí)行開(kāi)銷(xiāo)。 |
|
來(lái)自: 行者花雕 > 《待分類(lèi)》