单例模式详解--通过源码分析:反射及反序列化破坏单例原理及枚举式单例如果防止其破坏、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中的对象管理通过该方式

▄█▀█●各位同仁,如果我的代码对你有帮助,请给我一个赞吧,为了下次方便找到,也可关注加收藏呀
如果有什么意见或建议,也可留言区讨论

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