zoukankan      html  css  js  c++  java
  • 单例模式的挑战:反射和序列化

    参考1: 你写的单例模式,能防止反序列化和反射吗?

    参考2:枚举实现单例

    常见单例模式

    // 饿汉,在类加载的时候就被实例化
    /**
     * 恶汉式单例,线程安全
     * @author sicimike
     * @create 2020-02-23 20:15
     */
    public class Singleton1 {
    
        private static final Singleton1 INSTANCE = new Singleton1();
    
        private Singleton1() {}
    
        public static Singleton1 getInstance() {
            return INSTANCE;
        }
    }
    
    /**
     * 饿汉式单例,静态代码块,线程安全
     * @author sicimike
     * @create 2020-02-23 20:19
     */
    public class Singleton2 {
    
        private static Singleton2 INSTANCE = null;
    
        static {
            INSTANCE = new Singleton2();
        }
    
        private Singleton2() {}
    
        public static Singleton2 getInstance() {
            return INSTANCE;
        }
    }
    
    // 懒汉式单例
    /**
     * 懒汉式单例,线程安全
     * 双重校验锁
     * @author sicimike
     * @create 2020-02-23 20:34
     */
    public class Singleton6 {
    
        private static volatile Singleton6 INSTANCE = null;
    
        private Singleton6() {}
    
        public static Singleton6 getInstance() {
            if (INSTANCE == null) {
                synchronized (Singleton6.class) {
                    if (INSTANCE == null) {
                        INSTANCE = new Singleton6();
                    }
                }
            }
            return INSTANCE;
        }
    }
    

    不加volatile关键字 线程不安全,根本原因就是INSTANCE = new Singleton5()不是原子操作。而是分为三步完成
    1、分配内存给这个对象
    2、初始化这个对象
    3、把INSTANCE变量指向初始化的对象
    正常情况下按照1 -> 2 -> 3的顺序执行,但是2和3可能会发生重排序,执行顺序变成1 -> 3 -> 2。如果是1 -> 3 -> 2的顺序执行。线程A执行完3,此时对象尚未初始化,但是INSTANCE变量已经不为null,线程B执行到synchronized关键字外部的if判断时,就直接返回了。此时线程B拿到的是一个尚未初始化完成的对象,可能会造成安全隐患。所以这种实现方式是线程不安全的。

    volatile关键字的在这里的作用有两个:
    解决了重排序的问题
    保证了INSTANCE的修改,能够及时的被其他线程所知

    静态内部类方式
    既满足懒加载,又满足线程安全,代码量还少,相对来说是一种比较优雅的实现方式

    /**
     * 懒汉式单例,线程安全
     * 静态内部类
     * @author sicimike
     * @create 2020-02-23 20:36
     */
    public class Singleton7 {
    
        private Singleton7() {}
    
        public static Singleton7 getInstance() {
            return InnerClass.INSTANCE;
        }
    
        private static class InnerClass {
            private static Singleton7 INSTANCE = new Singleton7();
        }
    
    }
    

    枚举方式

    public enum  DataSourceEnum {
        DATASOURCE;
        private DBConnection connection = null;
        private DataSourceEnum(){
            connection = new DBConnection();
        }
        public DBConnection getConnection(){
            return connection;
        }
    }
    
    public class DBConnection {
    }
    
    // 测试 返回 true 
    public class Test {
        public static void main(String[] args) {
            DBConnection conn1 = DataSourceEnum.DATASOURCE.getConnection();
            DBConnection conn2 = DataSourceEnum.DATASOURCE.getConnection();
            System.out.println(conn1 == conn2);
        }
    }
    

    反射

    反射会破坏单例

    public void reflectSingleton1(){
        try {
    
            Object compare1 = Singleton1.getInstance();
            Class<?> tClass = Singleton1.class;
            Constructor constructor = tClass.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            Object instance = constructor.newInstance();
            System.out.println(instance);
            System.out.println(compare1);
            System.out.println(instance == compare1);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
    
    // 输出
    singleton.Singleton1@27f674d
    singleton.Singleton1@1d251891
    false
    
    // 添加如下报错内容处理,防止通过反射初始化单例对象,但是不够优雅
    private Singleton1() {
        if(INSTANCE != null){
            throw new RuntimeException("do not xia gao");
        }
    }    
    

    序列化

    序列化也会破坏单例,不再举例。
    可在Singleton1内添加如下方法

    private Object readResolve(){
        return this.INSTANCE;
    }
    

    经过源码查看,若目标类有readResolve方法,那就通过反射的方式调用要被反序列化的类中的readResolve方法,返回一个对象,然后把这个新的对象复制给最终返回的对象。
    因此,新建readResolve方法,返回单例类,即保证还是原来创建的类,没有创建新类,是一个对象。

    最优解:就是用枚举

    // 可以看一下DataSourceEnum类的反编译代码
    public final class DataSourceEnum extends Enum
    {
        public static DataSourceEnum[] values(){
            return (DataSourceEnum[])$VALUES.clone();
        }
    	//toString的逆方法,返回指定名字,给定类的枚举常量
        public static DataSourceEnum valueOf(String name){
            return (DataSourceEnum)Enum.valueOf(creational/singleton/dbconn/DataSourceEnum, name);
        }
    	//私有构造函数,参数有 此枚举常量的名称,枚举常量的序号
        private DataSourceEnum(String s, int i){
            super(s, i);
            //单例对象的属性
            connection = null;
            connection = new DBConnection();
        }
    
        public DBConnection getConnection(){
            return connection;
        }
    	//单例对象
        public static final DataSourceEnum DATASOURCE;
        //单例对象的属性
        private DBConnection connection;
        private static final DataSourceEnum $VALUES[];
        static 
        {
        	//与饿汉式相似,类初始化时创建单例对象
            DATASOURCE = new DataSourceEnum("DATASOURCE", 0);
            $VALUES = (new DataSourceEnum[] {
                DATASOURCE
            });
        }
    }
    

    Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。也就是说,序列化的时候只将DATASOURCE这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

  • 相关阅读:
    第一个win8应用的制作过程
    win8开发-Xaml学习笔记一
    梦想成为“老板”的第二天
    梦想成为“老板”的第一天
    HTTP请求
    linux常用命令
    HTML中常用的标签
    HTML基本结构
    记录Django的settings文件常用配置
    Oracle数据泵expdp、impdp
  • 原文地址:https://www.cnblogs.com/cuiyf/p/14867040.html
Copyright © 2011-2022 走看看