• 你写的单例真的安全吗?


    先来一个经典的双重校验的单例

    public class Singleton {
        private static volatile Singleton instances;
        private Singleton(){
        }
        public static Singleton getInstance(){
            if (instances == null)
            {
                synchronized (Singleton.class)
                {
                    if (instances == null)
                    {
                        instances = new Singleton();
                    }
                }
            }
            return instances;
        }
    }
    
    @Test
    public void testSingleton(){
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2);//true
    }
    

    这里看似没有什么问题,无论是单线程还是多线程获得的都是同一个对象,但是真的就没有问题了吗?

    反射攻击

    @Test
    public void testRreflexSingleton() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);//无视私有修饰符
        Singleton instance1 = (Singleton)constructor.newInstance( null);
        Singleton instance2 = (Singleton)constructor.newInstance( null);
        System.out.println(instance1 == instance2); // false
    }
    

    这里通过反射来调用构造方法,获取了多个不同的对象。

    既然是调用构造函数那就在构造函数中做一些工作:加锁 + 红绿灯

    public class Singleton {
        private static volatile Singleton instances;
        private static volatile boolean flag = false;
        private Singleton() throws Exception {
            System.out.println("构造了一次");
            synchronized (Singleton.class){
                if (flag){
                    throw new Exception("有人在进行反射攻击");
                }else {
                    flag = true;
                }
            }
    
        }
    
        public static Singleton getInstance() throws Exception {
            if (instances == null)
            {
                synchronized (Singleton.class)
                {
                    if (instances == null)
                    {
                        instances = new Singleton();
                    }
                }
            }
            return instances;
        }
    }
    

    这里成功的阻止了反射创建多个对象。但如果我使用反射修改了flag的值呢?

    @Test
    public void testRreflexSingletonPlus() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);//无视私有修饰符
        Field flag = Singleton.class.getDeclaredField("flag");
    
        Singleton instance1 = (Singleton)constructor.newInstance( null);
        flag.setAccessible(true);
        flag.set(Singleton.class,false);
        Singleton instance2 = (Singleton)constructor.newInstance( null);
        System.out.println(instance1 == instance2);  // false
    }
    

    这里得到的对象又是不相同的!!!!那有木有什么办法可以解决呢?答案是有的--Enum

    创建一个Enum类型的单例

    public enum EnumSingleton {
        INSTANCES;
        public static EnumSingleton getInstances(){
            return INSTANCES;
        }
    }
    
    @Test
    public void testEnumSingleton(){
        EnumSingleton instances1 = getInstances();
        EnumSingleton instances2 = getInstances();
    
        System.out.println(instances1 == instances2);// true
    }
    

    Joshua Bloch大神说过单元素的枚举类型已经成为实现Singleton的最佳方法,下面我来讨论一下为什么

    @Test
    public void testRreflexEnumSingleton() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<EnumSingleton> clazz = EnumSingleton.class;
        Constructor<EnumSingleton> declaredConstructor = clazz.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        EnumSingleton instance1 = declaredConstructor.newInstance();
        EnumSingleton instance2 = declaredConstructor.newInstance();
    
        System.out.println(instance1 == instance2);
    }
    

    这里尝试获取无参构造失败

    拿不到构造函数那还怎么创建对象啊?咱们看看是不是真的没有构造函数

    image-20210629145539421

    通过clazz.getDeclaredConstructors();查看所有的构造方法,然后发现它有一个(String,int)的构造函数,然后咱们尝试调用它。

    这里虽然可以调用构造函数,但抛出了 java.lang.IllegalArgumentException: Cannot reflectively create enum objects ,说明 newInstance() 拒绝为Enum类创建对象,我们来看看是不是这样。

    果然在newInstance(Object ... initargs)的源码中判断了当前类是否是ENUM类型的,如果是ENUM类型的直接抛出异常。

    总结

    为什么ENUM单例不会被反射破坏?

    通过反射API getDeclaredConstructors()查看所有的构造方法,然后发现有一个参数为(String,int)的构造函数,然后通过 Constructor<EnumSingleton> constructor getDeclaredConstructors(String.class,int.class)拿到构造方法。先通过 constructor.setAccessible(true)无视权限修饰符。然后通过constructor.newInstance("INSTANCES",1)尝试获取会抛异常

    因为在newInstance()的源码中创建对象之前会先判断一下当前类的否是ENUM类型的,如果是会抛出异常。

    序列化攻击

    还是以上面经典的双重校验的单例为例,若其实现了Serializable接口可能会被序列化攻击

    public class Singleton implements Serializable {
        
        private static volatile Singleton instances;
        
        private Singleton() {
        }
        
        public static Singleton getInstance() {
            if (instances == null)
            {
                synchronized (Singleton.class)
                {
                    if (instances == null)
                    {
                        instances = new Singleton();
                    }
                }
            }
            return instances;
        }
    }
    @Test
    public void testSerSingleton() throws Exception {
        Singleton instance1 = Singleton.getInstance();
    
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
        oos.writeObject(instance1);
        oos.flush();
        oos.close();
    
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
        Singleton instance2 = (Singleton) ois.readObject();
        ois.close();
    
        System.out.println(instance1 == instance2);// false
    }
    

    通过序列化与反序列化之后得到的两个实例时不一样的。

    通过添加 readResolve()方法解决

    private Object readResolve(){
        return instances;
    }
    

    再次测试发现得到的对象是一样的

    ENUM类型可以抵御系列化攻击吗

    答案是:可以

    在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.EnumvalueOf() 方法来根据名字查找枚举对象。

    也就是说,以上面枚举为例,序列化的时候只将 INSTANCES 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

  • 相关阅读:
    HHHOJ #153. 「NOI模拟 #2」Kotomi
    HHHOJ #151. 「NOI模拟 #2」Nagisa
    Luogu P5298 [PKUWC2018]Minimax
    Luogu P5368 [PKUSC2018]真实排名
    Luogu P5408 【模板】第一类斯特林数·行
    Codechef December Challenge 2019 Division 1
    AtCoder Grand Contest 040
    CSP2019游记(翻车记)
    Codeforces Round #594 (Div. 1)
    AtCoder Grand Contest 039
  • 原文地址:https://www.cnblogs.com/shaoyu/p/14978414.html
Copyright © 2020-2023  润新知