单例模式中存在的安全问题

线程安全问题

双重校验锁

public class Singleton {
    private static Singleton instance = null;

    // 私有构造函数
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

因为对象的创建过程并非是原子性的。在创建的过程中,由于指令重排的影响,导致多线程并发状态下,容易产生线程不安全的问题。

对象创建过程

正常过程

当虚拟机执行 instance = new Singleton这句代码时,会被分解成以下三个动作来执行:
在这里插入图片描述

异常过程

但是,这三个动作的执行顺序并非是一成不变的,有可能经过JVM和CPU的优化编译之后,这三个动作的执行顺序发生了改变,变成了这样:
在这里插入图片描述

异常举例
状态序列instance值线程P1线程P2
1null竞争到锁,开始初始化
2null执行1,给对象分配内存空间
3未初始化执行3,指向内存地址进入getInstance方法,发现instance不为null,返回未初始化的instance

此时线程P2拿到的instance未进行初始化操作,存在安全隐患。

将instance对象声明为volatile,即可禁止指令重排序操作,解决上述问题。

破坏单例特性

反射破坏单例模式

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1);
        System.out.println(instance2);

        /*通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)*/
        Class<Singleton> clazz = (Class<Singleton>) Class.forName("Singleton");
        Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true); // 跳过权限检查
        Singleton instance3 = c.newInstance();
        Singleton instance4 = c.newInstance();
        System.out.println("通过反射的方式获取的对象instance3:" + instance3);
        System.out.println("通过反射的方式获取的对象instance4:" + instance4);
    }
}
// output
Singleton@299a06ac
Singleton@299a06ac
通过反射的方式获取的对象instance3:Singleton@383534aa
通过反射的方式获取的对象instance4:Singleton@6bc168e5

如上,即通过getInstance方法,能够正常获取唯一的单例对象。但通过反射能够任意地创建新的单例类对象。

public class Singleton {
    private volatile static Singleton instance = null;

    // 私有构造函数
    private Singleton() throws Exception {
        if (null != Singleton.instance) {
            throw new Exception("请使用getInstance方法获取单例对象!");
        }
    }

    public static Singleton getInstance() throws Exception {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
// output
Singleton@299a06ac
Singleton@299a06ac
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at Main.main(Main.java:188)
Caused by: java.lang.Exception: 请使用getInstance方法获取单例对象!
	at Singleton.<init>(Singleton.java:11)
	... 5 more

通过改造构造函数,能够解决已有单例对象情况下,通过反射创建单例对象的情况。

public class Main {
    public static void main(String[] args) throws Exception {
        /*通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)*/
        Class<Singleton> clazz = (Class<Singleton>) Class.forName("Singleton");
        Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true); // 跳过权限检查
        Singleton instance1 = c.newInstance();
        Singleton instance2 = c.newInstance();
        System.out.println("通过反射的方式获取的对象instance1:" + instance1);
        System.out.println("通过反射的方式获取的对象instance2:" + instance2);

    }
}
//output
通过反射的方式获取的对象instance1:Singleton@299a06ac
通过反射的方式获取的对象instance2:Singleton@383534aa

若是不调用getInstance方法,只通过反射创建单例对象,仍会引发同样的问题。

在单例类中加入flag标志位,可以解决,但反射能够改变成员变量的值,所以单例特性仍然能够被反射机制破坏。

解决方案

  1. 使用饿汉模式的单例
    饿汉模式,在类加载时即初始化单例对象,再通过反射对单例类进行实例化,会引发异常。

  2. 使用枚举方式的单例
    枚举单例类,java.lang.IllegalArgumentException: Cannot reflectively create enum objects

反序列化破坏单例模式

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton instance1 = Singleton.getInstance();
        try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("object"));
             ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("object"))) {

            //将对象持久化到磁盘中
            outputStream.writeObject(instance1);
            outputStream.flush();

            //从磁盘中反序列化成对象
            Singleton instance2 = (Singleton) inputStream.readObject();

            System.out.println("通过getInstance方法获取的对象instance1:" + instance1);
            System.out.println("通过反序列化的方式获取的对象instance2:" + instance2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// output 
通过getInstance方法获取的对象instance1:Singleton@27f674d
通过反序列化的方式获取的对象instance2:Singleton@48140564

显然,通过反序列方式得到的单例类对象,与原始的单例类对象已经不一样了。

阅读源码ObjectInputStream类发现原因

    /**
     * Reads and returns "ordinary" (i.e., not a String, Class,
     * ObjectStreamClass, array, or enum constant) object, or null if object's
     * class is unresolvable (in which case a ClassNotFoundException will be
     * associated with object's handle).  Sets passHandle to object's assigned
     * handle.
     */
    private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
        		// 如果实现了构造函数通过反射生成新对象
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);
				// 如果实现了ReadResolve方法,则通过该方法生成对象
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

反序列化的类对象首先通过反射生成新对象,如果实现了ReadResolve方法,就替换为该方法返回的对象,如果未实现,则反序列化得到的是一个全新的对象。

那么实现了ReadSolve方法,即可解决上述问题。

public class Singleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private volatile static Singleton instance = null;

    // 私有构造函数
    private Singleton() throws Exception {
        if (null != Singleton.instance) {
            throw new Exception("请使用getInstance方法获取单例对象!");
        }
    }

    public static Singleton getInstance() throws Exception {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    //添加的readResolve方法
    private Object readResolve() {
        return instance;
    }
}
// output
通过getInstance方法获取的对象instance1:Singleton@27f674d
通过反序列化的方式获取的对象instance2:Singleton@27f674d

一开始我认为,在单例类对象已经生成的情况下,通过反射实例化对象会抛出异常,但经过debug,发现最终通过如下代码,即使用native的方法分配了内存空间,并没有通过构造函数生成对象,也就跳过了构造函数的检查过程。

class BootstrapConstructorAccessorImpl extends ConstructorAccessorImpl {
    private final Constructor<?> constructor;

    BootstrapConstructorAccessorImpl(Constructor<?> var1) {
        this.constructor = var1;
    }

    public Object newInstance(Object[] var1) throws IllegalArgumentException, InvocationTargetException {
        try {
            return UnsafeFieldAccessorImpl.unsafe.allocateInstance(this.constructor.getDeclaringClass());
        } catch (InstantiationException var3) {
            throw new InvocationTargetException(var3);
        }
    }
}

解决方案

  1. 实现ReadSolve方法
  2. 使用枚举方式的单例
    枚举单例类,反序列化时,是通过枚举类的ValueOf方法进行实例化,不会出现上述问题。

总结

枚举是最好的单例实现。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值