ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

你写的单例真的安全吗?

2021-07-06 19:01:44  阅读:193  来源: 互联网

标签:Singleton 序列化 instances class 安全 单例 constructor 真的 public


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

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

标签:Singleton,序列化,instances,class,安全,单例,constructor,真的,public
来源: https://www.cnblogs.com/shaoyu/p/14978414.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有