簡單介紹七種單例模式

在這裏插入圖片描述

前言

單例模式相對來說,設計比較簡單,但是實現方式多種多樣,我們需要從線程安全、高性能、懶加載方面進行評估。

餓漢式

實例代碼如下:

public final class Singleton1 {

    private byte[] data = new byte[1024];

    private static Singleton1 instance = new Singleton1();

    private Singleton1() {
        System.out.println("Singleton1 實例化");
    }

    public static Singleton1 getInstance() {
        return instance;
    }
}

根據前面可以知道,在類的初始化階段類變量進行初始化,就是instance變量會被初始化,同時呢1k的空間data也被創建,我們可以寫段代碼測試下:

public class Test {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Class<?> aClass1 = Class.forName("fast.cloud.nacos.juc.singleton.Singleton1");
    }
}

運行代碼會輸出:

Singleton1 實例化

這也說明了Class.forName方法會對類進行初始化

關於這點不懂得可以看看前面的博客,有解釋到的,Class.forName 和 ClassLoader的區別

如果一個類屬性較少,佔用內存也較小,用餓漢式也未嘗不可。

總而言之,餓漢式不支持懶加載,可以保證多線程下只被加載一次,性能也比較高。

懶漢式

所謂懶漢式就是當你使用的時候再去創建,代碼如下


public final class Singleton2 {
    private byte[] data = new byte[1024];

    private static Singleton2 instance = null;

    private Singleton2() {
        System.out.println("Singleton2 實例化");
    }

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

這種方式會有一個問題,當兩個線程都走在 instance == null時,這個時候會創建兩個實例,線程不安全。

懶漢式 + 同步方法

代碼如下


public final class Singleton3 {

    private byte[] data = new byte[1024];

    private static Singleton3 instance = null;

    private Singleton3() {
        System.out.println("Singleton3 實例化");
    }

    //加鎖,每次只有一個線程獲得
    public static synchronized Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}

這種方式的問題在於,synchronized的排他性,同一時刻,只能被一個線程訪問,性能低下。

Double-Check

這種方法就是,在初次初始化的時候進行加鎖,之後就需要加鎖了,提高了效率。


public final class Singleton4 {
    private byte[] data = new byte[1024];

    private static Singleton4 instance = null;

    private Singleton4() {
        System.out.println("Singleton4 實例化");
    }

    public static  Singleton4 getInstance() {
        if (instance == null) {
            synchronized (Singleton4.class) {
                if (instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

這種會出現空指針異常,我們來逐步分析下:

主要在於singleton = new Singleton4()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
  1. 給 singleton 分配內存
  2. 調用 Singleton 的構造函數來初始化成員變量,形成實例
  3. 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null了)

在JVM的即時編譯器中存在指令重排序的優化。

也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,
被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。

解決方案就是加上volatile,就可以了


public final class Singleton5 {

    private byte[] data = new byte[1024];

    /**
     * static 和 volatile 切換,編譯器不會報錯
     */
    private static volatile Singleton5 instance = null;

    private Singleton5() {
        System.out.println("Singleton5 實例化");
    }

    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                if (instance == null) {
                    instance = new Singleton5();
                }
            }
        }
        return instance;
    }
}

這了需要注意
volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操作([1-2-3])完成之前,
不會調用讀操作(if (instance == null))。

Holder方式

Holder方式借住類加載的特點,同一類加載器只會對同一個類加載一次(這個時候可能被問到類加載器底層如何保證只被加載一次,可以翻翻我前面的博客)


public final class Singleton6 {

    private byte[] data = new byte[1024];


    private static final class Holder {
        private static Singleton6 instance = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return Holder.instance;
    }
}


Holder會在編譯的時候被收集到<clinit>方法中,該方法可以保證線程安全。

Singleton6也可以保證懶加載,只有在初次調用getInstance方法時,纔會初始化Holder中的實例

枚舉方式

利用自動序列化機制,保證了線程的絕對安全


public enum Singleton7 {
    INSTANCE;

    Singleton7() {
        System.out.println("Singleton7 實例化");
    }

    public static void method() {
        //調用該方法,會導致初始化
    }

    public static Singleton7 getInstance() {
        return INSTANCE;
    }
}

這種方法如果調用了外部調用了靜態方法也會初始化,可以加上我們的Holder模式

枚舉-Holder


public class Singleton8 {

    private byte[] data = new byte[1024];

    private Singleton8() {

    }
    public static void method() {
        //調用該方法,不會導致初始化
    }

    private enum EnumHolder{
        INSTANCE;

        private  Singleton8 instance;
        private EnumHolder() {
            System.out.println("Singleton8 實例化");
            this.instance = new Singleton8();
        }

        public Singleton8 getInstance() {
            return instance;
        }
    }

    public Singleton8 getInstance() {
        return EnumHolder.INSTANCE.getInstance();
    }


}

總結

開發過程中,個人用的比較多的也是Holder模式和枚舉模式吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章