你写的单例真的安全吗?
先来一个经典的双重校验的单例
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.Enum
的 valueOf()
方法来根据名字查找枚举对象。
也就是说,以上面枚举为例,序列化的时候只将 INSTANCES 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。