你的單例真的知足需求嗎

前言

在面試中被問到頻率最高的設計模式是單例,由於它寫起來很簡單,並且瞭解單例模式的都知道,它有餓漢式、懶漢式、DCL(雙重鎖判斷)、靜態內部類以及枚舉等多種寫法。但說實話,在實際應用中,單例用到的並非不少。但做爲設計模式的基本模式之一,咱們也有必要了解單例是否知足需求,例如線程是否安全,是否延遲加載,反射是否安全,序列化是否安全,這是本文重點關注的問題。
單例模式就是在應用的整個生命週期中只存在一個實例。它有不少好處,避免實例對象的重複建立,減小實例對象的重複建立,減小系統開銷。例如spring容器中管理的Bean默認就是單例的。java

五種單例模式

餓漢式

寫法

public class HungrySingleton implements Serializable{

    private static HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return singleton;
    }

}
複製代碼

之因此implements Serializable(下同),是爲了後面測試序列化是否安全的須要,通常狀況不用加。面試

特性

餓漢式在類加載時期就已經初始化實例,而咱們知道類加載是線程安全的,因此餓漢式是線程安全的。很明顯,它不是延遲加載的,這也是餓漢式的缺點。經過下面的測試方法1,餓漢式不是反射安全的,由於經過反射構造方法產生了兩個實例。經過測試方法2,餓漢式也不是序列化安全的。spring

測試方法1:設計模式

public static void main(String [] args) {
        //測試餓漢式反射是否安全
        reflectTest();
    }
    
    private static void reflectTest() {
        HungrySingleton singleton1 = HungrySingleton.getInstance();
        HungrySingleton singleton2 = null;
        try {
            Class<HungrySingleton> clazz = HungrySingleton.class;
            Constructor<HungrySingleton> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            singleton2 = constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
複製代碼

運行結果:安全

測試方法2:bash

public static void main(String [] args) {
        //測試餓漢式序列化是否安全
        serializableTest();
    }

    private static void serializableTest() {
        HungrySingleton singleton1 = HungrySingleton.getInstance();
        HungrySingleton singleton2 = null;
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:\\1.txt")));
            outputStream.writeObject(singleton1);
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:\\1.txt")));
            singleton2 = (HungrySingleton) inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
複製代碼

運行結果: 多線程

懶漢式

寫法

public class LazySingletonThreadNotSafe implements Serializable{

    private static LazySingletonThreadNotSafe singleton = null;

    private LazySingletonThreadNotSafe() {
        
    }
    public static LazySingletonThreadNotSafe getSingleton() {
        if (singleton == null) {
            singleton = new LazySingletonThreadNotSafe();
        }
        return singleton;
    }

}
複製代碼

特性

懶漢式在餓漢式的基礎上進行了改造,將實例的初始化從類加載過程移到getInstance()方法真正調用時進行。因此具有了延遲加載,但失去了線程安全性。下面的DCL在此基礎上增長了線程安全。從測試方法1和2可知,懶漢式反射不安全,序列化也不安全。 測試方法1:工具

public static void main(String [] args) {
        //測試懶漢式反射是否安全
        reflectTest();
    }

    private static void reflectTest() {
        LazySingletonThreadNotSafe singleton1 = LazySingletonThreadNotSafe.getSingleton();
        LazySingletonThreadNotSafe singleton2 = null;
        try {
            Class<LazySingletonThreadNotSafe> clazz = LazySingletonThreadNotSafe.class;
            Constructor<LazySingletonThreadNotSafe> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            singleton2 = constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
複製代碼

運行結果:測試

測試方法2:ui

public static void main(String [] args) {
        //測試懶漢式序列化是否安全
        serializableTest();
    }

    private static void serializableTest() {
        LazySingletonThreadNotSafe singleton1 = LazySingletonThreadNotSafe.getSingleton();
        LazySingletonThreadNotSafe singleton2 = null;
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:\\1.txt")));
            outputStream.writeObject(singleton1);
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:\\1.txt")));
            singleton2 = (LazySingletonThreadNotSafe) inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }

複製代碼

運行結果:

DCL(雙重鎖判斷Double Check Lock)

寫法

public class LazySingletonThreadSafe implements Serializable{

    private volatile static LazySingletonThreadSafe singleton =null;

    private LazySingletonThreadSafe() {
        
    }

    public static LazySingletonThreadSafe getSingleton() {
        if (singleton == null) {   //1
            synchronized (LazySingletonThreadSafe.class) {   //2
                if (singleton == null) {   //3
                    singleton = new LazySingletonThreadSafe();  //4
                }
            }
        }
        return singleton;
    }
}
複製代碼

特性

DCL是在懶漢式基礎上的改進,跟懶漢式惟一不一樣的是DCL是線程安全的。你可能會問,有了synchronized保證線程安全,爲啥還要加volatile修飾?由於DCL自己存在一個致命缺陷,就是重排序致使的多線程訪問可能得到一個未初始化的對象。
咱們知道singleton = new LazySingletonThreadSafe();這行代碼在JVM看來有這麼三步:
一、爲對象分配存儲空間
二、初始化對象
三、將singleton引用指向第一步中分配的內存地址
第2步和第3步可能存在重排序。假設線程A按二、3步顛倒的順序執行代碼(發生了重排序),先執行了第3步,此時singleton引用已經指向了第一步中分配的內存地址,當線程B執行getSingleton()方法時,發現singleton != null,就執行得到了尚未初始化的singleton,這樣就出問題了。咱們知道volatile的性質是保證多線程環境下變量的可見性以及禁止指令重排序,因此要加volatile。

靜態內部類

寫法

public class StaticInnerSingleton implements Serializable{

private StaticInnerSingleton() {

}

/**
 * 靜態內部類,它和餓漢式同樣,基於類加載機制的線程安全,又作到延遲加載。
 * SingletonHolder是一個內部類,當外部類StaticInnerSingleton被加載的時候不會被加載,
 * 調用getSingleton方法的時候纔會被加載。
 */
private static class SingletonHolder {
    private static final StaticInnerSingleton singleton = new StaticInnerSingleton();
}

public static StaticInnerSingleton getSingleton() {
    return SingletonHolder.singleton;
}
複製代碼

}

特性

靜態內部類和餓漢式同樣是線程安全的,同時又作到了延遲加載。可是反射不安全,序列化也不安全。
測試方法1:

public static void main(String [] args) {
        //測試靜態內部類反射是否安全
        reflectTest();
    }

    private static void reflectTest() {
        StaticInnerSingleton singleton1 = StaticInnerSingleton.getSingleton();
        StaticInnerSingleton singleton2 = null;
        try {
            Class<StaticInnerSingleton> clazz = StaticInnerSingleton.class;
            Constructor<StaticInnerSingleton> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            singleton2 = constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }

複製代碼

運行結果:

測試方法2:

public static void main(String [] args) {
        //測試靜態內部類序列化是否安全
        serializableTest();
    }

    private static void serializableTest() {
        StaticInnerSingleton singleton1 = StaticInnerSingleton.getSingleton();
        StaticInnerSingleton singleton2 = null;
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:\\1.txt")));
            outputStream.writeObject(singleton1);
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:\\1.txt")));
            singleton2 = (StaticInnerSingleton) inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
複製代碼

運行結果:

枚舉

寫法(簡單)

public enum EnumInstance implements Serializable{
    INSTANCE;
}
複製代碼

特性

用java反編譯工具看看Enum的源碼,跟餓漢式同樣,是在類加載時就初始化了,是線程安全的,因此並非延遲加載的。

public final class EnumSingleton extends Enum {

    public static EnumSingleton[] values() {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String s) {
        return (EnumSingleton)Enum.valueOf(test/singleton/EnumSingleton, s);
    }

    private EnumSingleton(String s, int i) {
        super(s, i);
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];

    static {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}
複製代碼

測試方法:

public static void main(String [] args) {
        //測試枚舉反射是否安全
        reflectTest();
    }

    private static void reflectTest() {
        EnumInstance singleton1 = EnumInstance.INSTANCE;
        EnumInstance singleton2 = null;
        try {
            Class<EnumInstance> clazz = EnumInstance.class;
            Constructor<EnumInstance> constructor = clazz.getDeclaredConstructor(String.class,int.class);
            constructor.setAccessible(true);
            singleton2 = constructor.newInstance("test",1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製代碼

運行結果:

直接不讓反射了,說明枚舉是反射安全的。在 constructor.newInstance()源碼中,有這麼幾行,是枚舉類型直接拋異常了。最後枚舉單例也是序列化安全的,能夠本身測試一下。

總結

經過以上測試,瞭解了五種單例模式各有優缺點,沒有說哪一種單例模式最好,只有知足需求的纔是最合適的。

相關文章
相關標籤/搜索