作java開發的能夠看看,Java單例模式的8種寫法總結!

單例:Singleton,是指僅僅被實例化一次的類。設計模式

1、餓漢設計模式

public class SingletonHungry {
    private final static SingletonHungry INSTANCE = new SingletonHungry();

    private SingletonHungry() {
    }

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

由於單例對象一開始就初始化了,不會出現線程安全的問題。安全

PS:由於咱們只須要初始化1次,因此給INSTANCE加了final關鍵字,代表初始化1次後再也不容許初始化。多線程

2、簡單懶漢設計模式

因爲餓漢模式一開始就初始化好了,但若是一直沒有被使用到的話,是會浪費珍貴的內存資源的,因此引出了懶漢模式。併發

懶漢:首次使用時纔會去實例化對象。性能

public class SingletonLazy1 {
    private static SingletonLazy1 instance;

    private SingletonLazy1() {
    }

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

測試:測試

public class Main {
    public static void main(String[] args) {
        SingletonLazy1 instance1 = SingletonLazy1.getInstance();
        SingletonLazy1 instance2 = SingletonLazy1.getInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

測試結果:從結果能夠看出,打印出來的兩個實例對象地址是同樣的,因此認爲是隻建立了一個對象。
線程

3、進階1:解決多線程併發問題

上述代碼存在的問題:在多線程環境下,不能保證只建立一個實例,咱們進行問題的重現:設計

public class Main {
    public static void main(String[] args) {
        new Thread(()-> System.out.println(SingletonLazy1.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy1.getInstance())).start();
    }
}

結果:獲取到的對象不同,這並非咱們的預期結果。3d

解決方案:指針

public class SingletonLazy2 {
    private static SingletonLazy2 instance;

    private SingletonLazy2() {
    }
    //在方法加synchronized修飾符
    public static synchronized SingletonLazy2 getInstance() {
        if (instance == null) {
            instance = new SingletonLazy2();
        }
        return instance;
    }
}

測試:

public class Main2 {
    public static void main(String[] args) {
        new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
    }
}

結果:多線程環境下獲取到的是同個對象。

4、進階2:縮小方法鎖粒度

上一方案雖然解決了多線程問題,但因爲synchronized關鍵字是加在方法上的,鎖粒度很大,當有上萬甚至更多的線程同時訪問時,都被攔在了方法外,大大下降了程序性能,因此咱們要適當縮小鎖粒度,控制鎖的範圍在代碼塊上。

public class SingletonLazy3 {
    private static SingletonLazy3 instance;

    private SingletonLazy3() {
    }

    public static SingletonLazy3 getInstance() {
        //代碼塊1:不要在if外加鎖,否則和鎖方法沒什麼區別
        if (instance == null) {
            //代碼塊2:加鎖,將方法鎖改成鎖代碼塊
            synchronized (SingletonLazy3.class) {
                //代碼塊3
                instance = new SingletonLazy3();
            }
        }
        return instance;
    }
}

測試:

public class Main3 {
    public static void main(String[] args) {
        new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
    }
}

咱們看一下運行結果:仍是出現了線程安全的問題(每次執行均可能打印不一樣的地址狀況,只要證實是非線程安全的便可)。

緣由分析:當線程A拿到鎖進入到代碼塊3而且尚未建立完實例時,線程B是有機會到達代碼塊2的,此時線程C和D可能在代碼塊1,當線程A執行完以後釋放鎖並返回對象1,線程B進入進入代碼塊3,又建立了新的對象2覆蓋對象1並返回,最後當線程C和D在進行判null時發現instance非空,直接返回最後建立的對象2。

5、進階3:雙重檢查鎖DCL(Double-Checked-Locking)

所謂雙重檢查鎖,就是在線程獲取到鎖以後再對實例進行第2次判空檢查,判斷是否是有上一個線程已經進行了實例化,有的話直接返回便可,不然進行實例初始化。

public class SingletonLazy4DCL {
    private static SingletonLazy4DCL instance;

    private SingletonLazy4DCL() {
    }

    public static SingletonLazy4DCL getInstance() {
        //代碼塊1:第一次判空檢查
        if (instance == null) {
            //代碼塊2:加鎖,將方法鎖改成鎖代碼塊
            synchronized (SingletonLazy3.class) {
                //代碼塊3:進行第二次(雙重)判空檢查
                if (instance == null) {
                    instance = new SingletonLazy4DCL();
                }
            }
        }
        return instance;
    }
}

測試:

public class Main4DCL {
    public static void main(String[] args) {
        new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
    }
}

6、進階4:禁止指令重排

在對象的實例過程當中,大概可分爲如下3個步驟:

  1. 分配對象內存空間
  2. 在空間中建立對象
  3. 實例指向分配到的內存空間地址

因爲實例化對象的過程不是原子性的,且JVM自己對Java代碼指令有重排的操做,可能1-2-3的操做被從新排序成了1-3-2,這樣就會致使在3執行完以後還沒來得及建立對象時,其餘線程先讀取到了未初始化的對象instance並提早返回,在使用的時候會出現NPE空指針異常。

解決:給instance加volatile關鍵字代表禁止指令重排,出現的機率不大, 但這是更安全的一種作法。

public class SingletonLazy5Volatile {
    //加volatile關鍵字
    private volatile static SingletonLazy5Volatile instance;

    private SingletonLazy5Volatile() {
    }

    public static SingletonLazy5Volatile getInstance() {
        //代碼塊1
        if (instance == null) {
            //代碼塊2:加鎖,將方法鎖改成鎖代碼塊
            synchronized (SingletonLazy3.class) {
                //代碼塊3
                if (instance == null) {
                    instance = new SingletonLazy5Volatile();
                }
            }
        }
        return instance;
    }
}

7、進階5:靜態內部類

咱們還可使用靜態類的靜態變量被第一次訪問時纔會進行初始化的特性來進行懶加載初始化。把外部類的單例對象放到靜態內部類的靜態成員變量裏進行初始化。

public class SingletonLazy6InnerStaticClass {
    private SingletonLazy6InnerStaticClass() {
    }

    public static SingletonLazy6InnerStaticClass getInstance() {
        return SingletonLazy6InnerStaticClass.InnerStaticClass.instance;
        //或者寫成return InnerStaticClass.instance;
    }

    private static class InnerStaticClass {
        private static final SingletonLazy6InnerStaticClass instance = new SingletonLazy6InnerStaticClass();
    }
}

雖然靜態內部類裏的寫法和餓漢模式很像,但它卻不是在外部類加載時就初始化了,而是在第一次被訪問到時纔會進行初始化的操做(即getInstance方法被調用時),也就起到了懶加載的效果,而且它能夠保證線程安全。

測試:

public class Main6InnerStatic {
    public static void main(String[] args) {
        new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
        new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
    }
}

反射攻擊

雖然咱們一開始都對構造器進行了私有化處理,但Java自己的反射機制卻仍是能夠將private訪問權限改成可訪問,依舊能夠建立出新的實例對象,這裏以餓漢模式舉例說明:

public class MainReflectAttack {
    public static void main(String[] args) {
        try {
            SingletonHungry normal1 = SingletonHungry.getInstance();
            SingletonHungry normal2 = SingletonHungry.getInstance();
            //開始反射建立實例
            Constructor<SingletonHungry> reflect = SingletonHungry.class.getDeclaredConstructor(null);
            reflect.setAccessible(true);
            SingletonHungry attack = reflect.newInstance();

            System.out.println("正常靜態方法調用獲取到的對象:");
            System.out.println(normal1);
            System.out.println(normal2);
            System.out.println("反射獲取到的對象:");
            System.out.println(attack);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

8、枚舉單例(推薦使用)

public enum SingletonEnum {
    INSTANCE;
}

枚舉是最簡潔、線程安全、不會被反射建立實例的單例實現,《Effective Java》中也代表了這種寫法是最佳的單例實現模式。

單元素的枚舉類型常常成爲實現Singleton的最佳方法。 --《Effective Java》

爲何說不會被反射建立對象呢?查閱構造器反射實例化對象方法newInstance的源碼可知:反射禁止了枚舉對象的實例化,也就防止了反射攻擊,不用本身在構造器實現複雜的重複實例化邏輯了。

測試:

public class MainEnum {
    public static void main(String[] args) {
        SingletonEnum instance1 = SingletonEnum.INSTANCE;
        SingletonEnum instance2 = SingletonEnum.INSTANCE;
        System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());
    }
}

總結:幾種實現方式的優缺點

懶漢模式
  • 優勢:節省內存。
  • 缺點:存在線程安全問題,若要保證線程安全,則寫法複雜。
餓漢模式
  • 優勢:線程安全。
  • 缺點:若是單例對象一直沒被使用,則會浪費內存空間。
靜態內部類
  • 優勢:懶加載並避免了多線程問題,寫法相比於懶漢模式更簡單。
  • 缺點:須要多建立一個內部類。
枚舉
  • 優勢:簡潔、天生線程安全、不可反射建立實例。
  • 缺點:暫無
相關文章
相關標籤/搜索