單例模式詳解--通過源碼分析:反射及反序列化破壞單例原理及枚舉式單例如果防止其破壞、readResolve()如何防止反序列化破壞單例以及spring容器式單例思想

寫在前面

本文從最基礎的餓漢式及懶漢式demo進行引入,通過jdk源碼分別分析了:反射及反序列化破壞單例原理、readResolve()如何防止反序列化破壞單例、枚舉式單例的優點及如何防止反射及反序列化破壞、以及spring容器式單例思想詳解。

餓漢式單例模式:

一般形式

/**
 * 優點:執行效率高,性能高,沒有任何的鎖
 * 缺點:某些情況下,可能會造成內存浪費
 */
public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){}

    public static HungrySingleton getInstance(){
        return  hungrySingleton;
    }
}

靜態代碼塊

/**
 * 靜態代碼塊
 */
public class HungryStaticSingleton {

    private static final HungryStaticSingleton hungrySingleton;

    static {
        hungrySingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton(){}

    public static HungryStaticSingleton getInstance(){
        return  hungrySingleton;
    }
}

懶漢式單例模式:

雙重檢查鎖

/**
 * 優點:性能高了,線程安全了
 * 缺點:可讀性難度加大,不夠優雅,並且加鎖會產生性能問題
 */
public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton instance;
    private LazyDoubleCheckSingleton(){}

    public static LazyDoubleCheckSingleton getInstance(){
        //檢查是否要阻塞
        if (instance == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                //檢查是否要重新創建實例
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                    //指令重排序的問題
                }
            }
        }
        return instance;
    }
}

靜態內部類

/*
   優點:寫法優雅,利用了Java本身語法特點,性能高,避免了內存浪費,不能被反射破壞
   缺點:不優雅
 */
public class LazyStaticInnerClassSingleton {

    private LazyStaticInnerClassSingleton(){
        if(LazyHolder.INSTANCE != null){
            throw new RuntimeException("不允許非法訪問");
        }
    }

    private static LazyStaticInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }

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

反射破壞單例

public class ReflectTest {

    public static void main(String[] args) {
        try {
            //獲取單例類的class及構造器
            Class<?> clazz = LazyDoubleCheckSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(null);

            //設置強制訪問
            c.setAccessible(true);

            //實例化兩次
            Object instance1 = c.newInstance();
            Object instance2 = c.newInstance();

            //分別打印
            System.out.println(instance1);
            System.out.println(instance2);

            //false
            System.out.println(instance1 == instance2);

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

輸出結果:在這裏插入圖片描述

序列化破壞單例:

//一個單例對象創建好後,有時候需要將對象序列化然後寫入磁盤,下次使用時再從磁盤中讀取對象進行反序列化,然後將其轉化爲內存對象。反序列化後的對象會重新分配內存,即重新創建
public class SeriableSingletonTest {
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.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 = (SeriableSingleton)ois.readObject();
            ois.close();

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

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class SeriableSingleton implements Serializable {

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

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

運行結果爲:
在這裏插入圖片描述

添加readResolve()方法

保證序列化不會破壞單例demo及運行結果

public class SeriableSingletonTest {
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.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 = (SeriableSingleton)ois.readObject();
            ois.close();

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

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class SeriableSingleton implements Serializable {

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    private Object readResolve(){
        return INSTANCE;
    }
}

運行結果
在這裏插入圖片描述

原理分析

寫在前面:雖然解決了單例模式被破壞的問題,但是實際上實例化了兩次,只不過新創建的對象沒有被返回,根據如下調用棧即可探明真相readObject()【ObjectInputStream】->readObject0(false)【ObjectInputStream】->readOrdinaryObject(unshared)【ObjectInputStream】

  • 先從demo中轉到如下代碼:s1 = (SeriableSingleton)ois.readObject();
  • 進入:readObject()【爲節省篇幅去掉了對此分析無關代碼】
//去掉無用代碼
public final Object readObject() { 
     Object obj = readObject0(false);
     handles.markDependency(outerHandle, passHandle);
     ClassNotFoundException ex = handles.lookupException(passHandle);
     if (ex != null) {
         throw ex;
     }
     if (depth == 0) {
         vlist.doCallbacks();
     }
     return obj;
       
    }
  • 進入:readObject0(false)【爲節省篇幅去掉了對此分析無關代碼】
private Object readObject0(boolean unshared) throws IOException {
      //去掉無用代碼
     switch (tc) {

         case TC_ENUM:
             return checkResolve(readEnum(unshared));

         case TC_OBJECT:
             return checkResolve(readOrdinaryObject(unshared));

         case TC_EXCEPTION:
             IOException ex = readFatalException();
             throw new WriteAbortedException("writing aborted", ex);

         default:
             throw new StreamCorruptedException(
                 String.format("invalid type code: %02X", tc));
     }
        
    }
  • 進入:readOrdinaryObject(unshared)【爲節省篇幅去掉了對此分析無關代碼】
    • 從該方法中我們可以看到先通過實現反序列化後的對象,如果單例對象中定義了readResolve()方法,則對前面生成的對象進行覆蓋,來保證單例。
    • 實際上實例化了兩次,只不過第二次實例化的對象沒有被返回而已
private Object readOrdinaryObject(boolean unshared) {
       
        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
        	//1. 實例化反序列化對象
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }


		//如果單例對象存在readResolve(),則對第一步【1. 實例化反序列化對象】產生的對象進行覆蓋
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
        	//進行覆蓋
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
        }

        return obj;
    }

註冊試單例模式

枚舉式單例模式

寫在前面:枚舉式單例模式,無法通過反射及反序列化來破壞單例,是實現單例模式最爲優良的方式,並且《Effective Java》一書也推薦使用枚舉來實現單例

代碼實現

public class EnumSingletonTest {
    public static void main(String[] args) {
        //構建兩個實例對象
        System.out.println("測試枚舉類型單例--start");
        EnumSingleton instance1 = EnumSingleton.getInstance();
        EnumSingleton instance2 = EnumSingleton.getInstance();
        System.out.println("是否爲同一對象:" + (instance1 == instance2));
        System.out.println();

        try {
            System.out.println("測試反射能夠破壞單例--start");
            //測試通過反射能否破壞單例
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
            c.setAccessible(true);
            Object o = c.newInstance();

        }catch (Exception e){
            System.out.println("發生異常,不允許通過反射構造Enum實例"+e.getMessage());
        }finally {
            System.out.println();
        }

        try {
            EnumSingleton enumSingleton1 = EnumSingleton.getInstance();
            EnumSingleton enumSingleton2 = null;
            System.out.println("測試反序列化能夠破壞單例--start");

            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
            oos.writeObject(enumSingleton1);
            oos.flush();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
            enumSingleton2 = (EnumSingleton)ois.readObject();

            System.out.println("是否爲同一對象:" + (enumSingleton1 == enumSingleton2));
        }catch (Exception e){
            System.out.println("發生異常,不允許通過反序列化破壞單例"+e.getMessage());
        }
    }
}
enum EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

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

運行結果
在這裏插入圖片描述

原理詳解

  • 爲什麼想要通過反射破環單例時,獲取構造方法時要傳入兩個參數呢?
    clazz.getDeclaredConstructor(String.class,int.class);
    查看java.lang.Enum類的源碼即可發現,其只含有這一個構造器在這裏插入圖片描述

  • Enum類型是如何防止反射破壞單例的,我們進入Constructor的newInstance()方法即可探明真相
    在這裏插入圖片描述

  • Enum類型是如何防止反序列化破壞單例的。

    • 寫在前面:因爲是通過類名及類對象找到唯一的枚舉類,所以不會產生多實例
    • 先通過此調用棧readObject()【ObjectInputStream】->readObject0(false)【ObjectInputStream】->readEnum(unshared)【ObjectInputStream】進入readEnum方法。可參見詳解readResolve()方法時的原理分析
    • ObjectInputStream.readEnum()方法如下,詳見代碼:Enum.valueOf((Class)cl, name);
private Enum<?> readEnum(boolean unshared) throws IOException {
       
        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: " + desc);
        }

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
         
            @SuppressWarnings("unchecked")
            //通過類名及類對象找到唯一的枚舉類
             Enum<?> en = Enum.valueOf((Class)cl, name);
             result = en;
            
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }
  • Enum.valueOf((Class)cl, name);方法如下:
 public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
 		//獲取存儲的Enum類對象
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

容器式單例模式

寫在前面:Enum實現單例雖有有衆多優點,但是當單例數量衆多時卻不方便管理。仿照spring思想,如果通過一個容器同一存儲則更方便管理,但是該方法實現的單例線程不安全也容易被破壞。

demo及運行結果

public class ContainerSingletonTest {
    public static void main(String[] args) {
        Object instance1 = ContainerSingleton.getInstance("singleton.container.ContainerSingleton");
        Object instance2 = ContainerSingleton.getInstance("singleton.container.ContainerSingleton");
        System.out.println(instance1 == instance2);
    }
}

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框架思想驗證

在這裏插入圖片描述

總結

  1. 餓漢式單例:類加載即初始化、線程安全,但是可以通過反射和反序列化破壞其單例,防止通過反射破壞其單例的方式爲:當實例化之後再調用構造函數時拋出異常
  2. 懶漢式單例:第一次調用進行初始化、線程不安全,需要通過雙重檢查鎖來實現線程安全,其他與餓漢式單例相同。
  3. 反射破壞單例原理:雖然構造器設置爲私有,但是可以通過設置強制訪問來調用其構造函數,具體爲:c.setAccessible(true);
  4. 序列化破壞單例原理:反序列化後的對象會重新分配內存,即重新創建
  5. readResolve()方法防止反序列化破壞單例原理:在反序列化調用readObject()方法中,會先反序列化一個實例,再進行判斷是否定義了該方法,如果定義了該方法,則將剛纔反序列化生成的對象進行覆蓋。其實實際上實例化了兩次,只不過新創建的對象沒有被返回
  6. 枚舉式單例模式:枚舉式單例模式,無法通過反射及反序列化來破壞單例。無法通過反射破壞單例是因爲jdk底層做了限制,當發現反射調用的是枚舉的構造器時,會拋出“”異常;無法反序列化來破環單例是因爲反序列化時如果該Enum類已被實例化則通過類名及類對象找到該枚舉類並返回,所以不會產生多實例。是實現單例模式最爲優良的方式,並且《Effective Java》一書也推薦使用枚舉來實現單例
  7. 容器式單例模式:方便於管理衆多的單例對象,但會出現線程安全問題,也會出現反射和反序列化破壞其單例的現象,不過spring中的對象管理通過該方式

▄█▀█●各位同仁,如果我的代碼對你有幫助,請給我一個贊吧,爲了下次方便找到,也可關注加收藏呀
如果有什麼意見或建議,也可留言區討論

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