深刻解析單例模式的寫法以及破壞單例方式

小夥們好,我是jack xu,今天跟你們講一個老生常談的話題,單例模式是最經常使用到的設計模式之一,熟悉設計模式的朋友對單例模式都不會陌生。網上的文章也不少,可是良莠不齊,參差不齊,要麼說的不到點子上,要麼寫的不完整,我試圖寫一篇史上最全單例模式,讓你看一篇文章就夠了。。java

單例模式定義及應用場景

單例模式是指確保一個類在任何狀況下都絕對只有一個實例,並提供一個全局訪問點。單例模式是建立型模式。許多時候整個系統只須要擁有一個全局對象,這樣有利於咱們協調系統總體的行爲。好比在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,而後服務進程中的其餘對象再經過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。咱們寫單例的思路是,隱藏其全部構造方法,提供一個全局訪問點。面試

餓漢式

這個很簡單,小夥們都寫過,這個在類加載的時候就當即初始化,由於他很餓嘛,一開始就給你建立一個對象,這個是絕對線程安全的,在線程還沒出現之前就實例化了,不可能存在訪問安全問題。他的缺點是若是不用,用不着,我都佔着空間,形成內存浪費。算法

public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}
複製代碼

還有一種是餓漢式的變種,靜態代碼塊寫法,原理也是同樣,只要是靜態的,在類加載的時候就已經成功初始化了,這個和上面的比起來沒什麼區別,無非就是裝個b,看起來比上面那種吊,由於見過的人很少嘛。spring

public class HungryStaticSingleton {

    private static final HungryStaticSingleton hungrySingleton;

    static {
        hungrySingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
    }

    public static HungryStaticSingleton getInstance() {
        return hungrySingleton;
    }

}
複製代碼

懶漢式

簡單懶漢

爲了解決餓漢式佔着茅坑不拉屎的問題,就產生了下面這種簡單懶漢式的寫法,一開始我先申明個對象,可是先不建立他,當用到的時候判斷一下是否爲空,若是爲空我就建立一個對象返回,若是不爲空則直接返回,爲何叫懶漢式,就是由於他很懶啊,要等用到的時候纔去建立,看上去很ok,可是在多線程的狀況下會產生線程安全問題。設計模式

public class LazySimpleSingleton {

    private static LazySimpleSingleton instance;

    private LazySimpleSingleton() {
    }

    public static LazySimpleSingleton getInstance() {
        if (instance == null) {
            instance = new LazySimpleSingleton();
        }
        return instance;
    }

}
複製代碼

若是有兩個線程同時執行到 if (instance==null) 這行代碼,這是判斷都會經過,而後各自會執行instance = new Singleton(),並各自返回一個instance,這時候就產生了多個實例,就沒有保證單例,以下圖所示。。 安全

怎麼解決這個問題呢,很簡單,加鎖啊,加一下synchronized便可,這樣就能保住線程安全問題了

雙重校驗鎖(DCL)

上面這樣寫法帶來一個缺點,就是性能低,只有在第一次進行初始化的時候才須要進行併發控制,然後面進來的請求不須要在控制了,如今synchronized加在方法上,我管你生成沒成生成,只要來了就得給我排隊,因此這種性能是極其低下的,那怎麼辦呢?咱們知道,其實synchronized除了加在方法上,還能夠加在代碼塊上,只要對生成對象的那一部分代碼加鎖就能夠了,由此產生一種新的寫法,叫作雙重檢驗鎖,咱們看下面代碼。。(ps:對於synchronized想要更深一步瞭解的同窗,能夠看我另外一篇文章bash

咱們看19行將synchronized包在了代碼塊上,當 singleton == null 的時候,咱們只對建立對象這一塊邏輯進行了加鎖控制,若是 singleton != null 的話,就直接返回,大大提高了效率。在21行的時候又加了一個singleton == null,這又是爲何呢,緣由是若是兩個線程都到了18行,發現是空的,而後都進入到代碼塊,這裏雖然加了synchronized,但做用只是進行one by one串行化,第一個線程往下走建立了對象,第二個線程等待第一個線程執行完畢後,我也往下走,因而乎又建立了一個對象,那仍是沒控制住單例,因此在21行當第二個線程往下走的時候在判斷一次,是否是被別的線程已經建立過了,這個就是雙重校驗鎖,進行了兩次非空判斷。

咱們看到在11行的時候加了 volatile 關鍵字,這是用來防止指令重排的,當咱們建立對象的時候會通過下面幾個步驟,可是這幾個步驟不是原子的,計算機比較聰明,有時候爲了提升效率他不是按順序1234執行的,多是3214執行。這時候若是第一個線程執行了instance = new LazyDoubleCheckSingleton(),因爲指令重排先進行了第三步,先分配了一個內存地址,第二個線程進來的時候發現對象已是非null,直接返回,但這時候對象還沒初始化好啊,第二個線程拿到的是一個沒有初始化好的對象!這個就是要加volatile的緣由。服務器

1.分配內存給這個對象
2.初始化對象
3.設置instance指向剛分配的內存地址
4.初次訪問對象
複製代碼

最後說下雙重校驗鎖,雖然提升了性能,可是在我看來不夠優雅,折騰來折騰去,一會防這一會防那,尤爲是對新手不友好,新手會不明白爲何要這麼寫。。多線程

靜態內部類

上面已經將鎖的粒度縮小到建立對象的時候了,但無論加在方法上仍是加在代碼塊上,終究仍是用到了鎖,只要用到鎖就會產生性能問題,那有沒有不用鎖的方式呢,答案是有的,那就是靜態內部類的方式,他實際上是利用了java代碼的一種特性,靜態內部類在主類加載的時候是不會被加載的,只有當調用getInstance()方法的時候纔會被加載進來進行初始化,代碼以下併發

/** * @author jack xu * 兼顧餓漢式的內存浪費,也兼顧synchronized性能問題 */
public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton() {
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
    }

}
複製代碼

好,講到這裏我已經介紹了五種單例的寫法,通過層層的演進推理,到第五種的時候已是很完美的寫法了,既兼顧餓漢式的內存浪費,也兼顧synchronized性能問題,那他真的必定完美嗎,其實否則,他還有一個安全的問題,接下來咱們講下單例的破壞,有兩種方式反射和序列化

單例的破壞

反射

咱們知道在上面單例的寫法中,在構造方法上加上private關鍵字修飾,就是爲了避免讓外部經過new的方式來建立對象,但還有一種暴力的方法,我就是不走尋常路,你不讓我new是吧,我反射給你建立出來,代碼以下

/** * @author jack xu */
public class ReflectDestroyTest {

    public static void main(String[] args) {
        try {
            Class<?> clazz = LazyInnerClassSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            Object o1 = c.newInstance();
            Object o2 = c.newInstance();
            System.out.println(o1 == o2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

上面c.setAccessible(true)就是強吻,你private了,我如今把你的權限設爲true,我照樣可以訪問,經過c.newInstance()調用了兩次構造方法,至關於new了兩次,咱們知道 == 比的是地址,最後結果是false,確實是建立了兩個對象,反射破壞單例成功。

那麼如何防止反射呢,很簡單,就是在構造方法中加一個判斷

public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton() {
        if (LazyHolder.INSTANCE != null) {
            throw new RuntimeException("不要試圖用反射破壞單例模式");
        }
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
    }
}
複製代碼

在看結果,防止反射成功,當調用構造方法時,發現單例實例對象已經不爲空了,拋出異常,不讓你在繼續建立了。。

序列化

接下來介紹單例的另外一種破壞方式,先在靜態內部類上實現Serializable接口,而後寫個測試方法測試下,先建立一個對象,而後把這個對象先序列化,而後在反序列化出來,而後對比一下

public static void main(String[] args) {

        LazyInnerClassSingleton s1 = null;
        LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();

        FileOutputStream fos = null;
        try {

            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (LazyInnerClassSingleton) ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製代碼

咱們來看結果發現是false,在進行反序列化時,在ObjectInputStream的readObject生成對象的過程當中,其實會經過反射的方式調用無參構造方法新建一個對象,因此反序列化後的對象和手動建立的對象是不一致的。

那麼怎麼避免呢,依然很簡單,在靜態內部類里加一個readResolve方法便可

private Object readResolve() {
        return LazyHolder.INSTANCE;
    }
複製代碼

在看結果就變成true了,爲何加了一個方法就能夠避免被序列化破壞呢,這裏不在展開,感興趣的小夥伴能夠看下ObjectInputStream的readObject()方法,一步步往下走,會發現最終會調用readResolve()方法。

至此,史上最牛b單例產生,已經無懈可擊、無可挑剔了。

枚舉

那麼這裏我爲何還要在介紹枚舉呢,在《Effective Java》中,枚舉是被推薦的一種方式,由於他足夠簡單,線程安全,也不會被反射和序列化破壞,你們看下才寥寥幾句話,不像上面雖然已經實現了最牛b的寫法,可是其中的過程很讓人煩惱啊,要考慮性能、內存、線程安全、破壞啊,一會這裏加代碼一會那裏加代碼,才能達到最終的效果。而使用枚舉,感興趣的小夥伴能夠反編譯看下,枚舉的底層其實仍是一個class類,而咱們考慮的這些問題JDK源碼其實幫咱們都已經實現好了,因此在 java 層面咱們只須要用三句話就能搞定!

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  
複製代碼

至此,我經過層層演進,由淺入深的給你們介紹了單例的這麼多寫法,從不完美到完美,這麼多也是網上很常見的寫法,下面我在送你們兩個彩蛋,擴展一下其餘寫單例的方式方法。。

彩蛋

容器式單例

容器式單例是咱們 spring 中管理單例的模式,咱們平時在項目中會建立不少的Bean,當項目啓動的時候spring會給咱們管理,幫咱們加載到容器中,他的思路方式方法以下。。

public class ContainerSingleton {
    private ContainerSingleton() {
    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className) {
        Object instance = null;
        if (!ioc.containsKey(className)) {
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return instance;
        } else {
            return ioc.get(className);
        }
    }
}
複製代碼

這個能夠說是一個簡易版的 spring 管理容器,你們看下這裏用一個map來保存對象,當對象存在的時候直接從map裏取出來返回出去,若是不存在先用反射建立一個對象出來,先保存到map中而後在返回出去。咱們來測試一下,先建立一個Pojo對象,而後兩次從容器中去取出來,比較一下,發現結果是true,證實兩次取出的對象是同一個對象。

可是這裏有一個問題,這樣的寫法是線程不安全的,那麼如何作到線程安全呢,這個留給小夥伴自行獨立思考完成。

CAS單例

從一道面試題開始:不使用synchronized和lock,如何實現一個線程安全的單例?咱們知道,上面講過的全部方式中,只要是線程安全的,其實都直接或者間接用到了synchronized,間接用到是什麼意思呢,就好比餓漢式、靜態內部類、枚舉,其實現原理都是利用藉助了類加載的時候初始化單例,即藉助了ClassLoader的線程安全機制。

所謂ClassLoader的線程安全機制,就是ClassLoader的loadClass方法在加載類的時候使用了synchronized關鍵字。也正是由於這樣, 除非被重寫,這個方法默認在整個裝載過程當中都是同步的,也就是保證了線程安全。

那麼答案是什麼呢,就是利用CAS樂觀鎖,他雖然名字中有個鎖字,但實際上是無鎖化技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試,代碼以下:

/** * @author jack xu */
public class CASSingleton {
    private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<CASSingleton>();

    private CASSingleton() {
    }

    public static CASSingleton getInstance() {
        for (; ; ) {
            CASSingleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }

            singleton = new CASSingleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}
複製代碼

在JDK1.5中新增的JUC包就是創建在CAS之上的,相對於對於synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現,他是一種基於忙等待的算法,依賴底層硬件的實現,相對於鎖它沒有線程切換和阻塞的額外消耗,能夠支持較大的並行度。雖然CAS沒有用到鎖,可是他在不停的自旋,會對CPU形成較大的執行開銷,在生產中咱們不建議使用,那麼爲何我還會講呢,由於這是工做擰螺絲,面試造火箭的典型!你能夠不用,可是你得知道,你說是吧。。

最後原創不易,若是你以爲寫的不錯,請點個贊!

相關文章
相關標籤/搜索