• 单例模式的挑战:反射和序列化


    参考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这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

  • 相关阅读:
    HDU2059(龟兔赛跑)
    pat 1012 The Best Rank
    pat 1010 Radix
    pat 1007 Maximum Subsequence Sum
    pat 1005 Sign In and Sign Out
    pat 1005 Spell It Right
    pat 1004 Counting Leaves
    1003 Emergency
    第7章 输入/输出系统
    第六章 总线
  • 原文地址:https://www.cnblogs.com/cuiyf/p/14867040.html
Copyright © 2020-2023  润新知