你写的单例真的安全吗?

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

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);
}

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

你写的单例真的安全吗?

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

你写的单例真的安全吗?

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

上一篇:山东大学暑期项目实训 第二周 3


下一篇:StartRecognition代码解析