深入理解单例模式

前言

​ 单例模式有很多种形式:饿汉式、懒汉式、DCL(双重校验)、静态内部类、容器单例、ThreadLocal单例,具体代码请查看单例模式的7种形式。本文着重记录下序列化、反射攻击对单例的破坏以及相应的解决方案,最后简单介绍下枚举单例在这两个方面的优势以及其实际应用。

序列化破坏单例

​ 一个栗子来看序列化对单例的破坏:

// 序列化对单例的破坏,以饿汉为例
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(hungrySingleton);
File file = new File("singleton");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); //c1

System.out.println(hungrySingleton);
System.out.println(newSingleton);

输出:HungrySingleton@776ec8df
​ HungrySingleton@26a1ab54

​ 可以看到hungrySingleton和newSingleton指向的是两个不同的对象,也就是这个单例模式创建了两个对象实例。

1

​ 以上是c1行执行的readObject方法调用链,执行readOrdinaryObject方法,通过反射获取到对象的Class,然后做出一个判断,如果是序列化的类就new一个instance。这就是造成生成了第二个对象的原因。

    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 {
            // isInstantiable 如果desc是序列化的类就new一个instance返回
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }
        ...
    }

    /**
     * Returns true if represented class is serializable/externalizable and can
     * be instantiated by the serialization runtime--i.e., if it is
     * externalizable and defines a public no-arg constructor, or if it is
     * non-externalizable and its first non-serializable superclass defines an
     * accessible no-arg constructor.  Otherwise, returns false.
     */
    boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }

防止单例破坏的解决办法

​ 如何防止序列化对单例的破坏,继续看readOrdinaryObject函数,new出这个instance之后,会继续执行一个判断,如果类是一个有readResolve方法的可序列化类,则会执行一个代码块。代码块里面的内容是通过动态代理的方式执行类的readResolve方法,并且用返回的对象将new出来的这个obj覆盖掉,并且返回覆盖之后的对象。

    private Object readOrdinaryObject(boolean unshared) throws IOException {
        ...
        Object obj;
        try {
            // isInstantiable 如果desc是序列化的类就new一个instance返回
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            ...
        }
        ...
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            // 如果desc是一个有readResolve方法的可序列化类返回true
            desc.hasReadResolveMethod()) 
        {
            //返回true会执行invokeReadResolve,找到readResolve方法并执行
            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);
                    }
                }
                // readResolve返回的对象覆盖掉原先创建的那个对象
                handles.setObject(passHandle, obj = rep);
            }
            ...
        }
        // 返回
        return obj;
    }

    /**
     * Returns true if represented class is serializable or externalizable and
     * defines a conformant readResolve method.  Otherwise, returns false.
     */
    boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
    }

    /** class-defined readResolve method, or null if none */
    private Method readResolveMethod;

    Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (readResolveMethod != null) {
            try {
            	// 执行方法
                return readResolveMethod.invoke(obj, (Object[]) null);
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof ObjectStreamException) {
                    throw (ObjectStreamException) th;
                } else {
                    throwMiscException(th);
                    throw new InternalError(th);  // never reached
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

​ 那么这个readResolveMethod是什么时候赋值为readResolve方法的呢?通过全局查找,发现ObjectStreamClass对象在构造的时候将readResolve放了进去。

    private ObjectStreamClass(final Class<?> cl) {
        ...
        writeReplaceMethod = getInheritableMethod(
            cl, "writeReplace", null, Object.class);
        readResolveMethod = getInheritableMethod(
            cl, "readResolve", null, Object.class);
        ...
    }

​ 查看调用栈发现,在一开始我们执行oos.writeObject(hungrySingleton);就执行了lookup函数对传进来的对象进行扫描,把它的私有域、非静态非抽象方法,另外如果类实现的是Externalizable接口(Serializable的子接口,可以自定义指定序列化哪些属性),会获取非静态私有方法。

2

​ 那么方法就显而易见了,在单例类中添加readResolve函数,并让它返回创建好的单例就行

public class HungrySingleton implements Serializable {

    private static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }

    // 注意不能是static的readResolve,否则获取不到
    private Object readResolve() {
        return instance;
    }
}

再次执行测试函数,得到结果:HungrySingleton@776ec8df
​ HungrySingleton@776ec8df

对单例的反射攻击

​ 还是以饿汉为例,反射可以获取单例模式私有的构造器,并且改变访问权限,所以private在反射下’‘变成了’'public。

		// 对单例的反射攻击        
		Class objClass = HungrySingleton.class;
        HungrySingleton instance = HungrySingleton.getInstance(); // c2
        Constructor constructor = objClass.getDeclaredConstructor();
        constructor.setAccessible(true); // 设置访问权限
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();// c3

        System.out.println(instance);
        System.out.println(newInstance);

​ 输出结果:HungrySingleton@eed1f14

​ HungrySingleton@1b28cdfa

反射攻击解决办法

​ 一个比较简单的解决方法是在私有构造器中做一层判断,判断当前单例对象是否已经存在,存在则抛出异常。

    private HungrySingleton() {
        // 简单防止反射攻击,适用于饿汉,静态内部类
        if (null != instance) {
            throw new RuntimeException("单例模式禁止反射调用");
        }
    }

​ 但是,这种方法仅适用于饿汉、静态内部类,因为这两个是在类加载的时候便创建单例对象,所以反射攻击必然在单例对象创建之后。而对于懒汉式,仍然在私有构造器中添加上述代码,并且将c2处的代码放到c3下面先利用反射获取一个对象,然后再创建单例,因为这个反射获取的对象引用并不指向单例里面的instance,所以创建了两个,而在多线程环境下更容易出现上述情况。

		// 对单例的反射攻击        
        Class objClass = LazySingleton.class;
        Constructor constructor = objClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingleton newInstance = (LazySingleton) constructor.newInstance(); // c3
        LazySingleton instance = LazySingleton.getInstance(); // c2

        System.out.println(instance);
        System.out.println(newInstance);

​ 输出结果:LazySingleton@7229724f

​ LazySingleton@4c873330

​ 那么创建计数或者使用信号量,并且在构造器中加以判断?别忘了反射也能访问成员变量等。

    private static boolean flag = true;
    private LazySingleton(){
        if (flag) {
            flag = false;
        } else {
            throw new RuntimeException("单例模式禁止反射调用");
        }
    }
    public static void main(String[] args) throws Exception {
		Class objClass = LazySingleton.class;
        Constructor constructor = objClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingleton newInstance = (LazySingleton) constructor.newInstance(); // c3
        // 在用getInstance获取单例之前,先用反射把false掷回true
        Field flag = objClass.getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(newInstance, true);
        LazySingleton instance = LazySingleton.getInstance(); // c2
    }

​ 输出结果还是:LazySingleton@7229724f

​ LazySingleton@4c873330

枚举式单例

​ 在Effective Java中推荐的枚举式单例模式,既能防止反射攻击又能解决序列化的问题

public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}
枚举式单例的序列化

​ 测试方法

	public static void main(String[] args) throws Exception {
        // 枚举类单例的序列化
        EnumSingleton enumSingleton = EnumSingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new 	FileOutputStream("file"));
        oos.writeObject(enumSingleton);
        File file = new File("file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        EnumSingleton newEnumSingleton = (EnumSingleton) ois.readObject();

        System.out.println(enumSingleton);
        System.out.println(newEnumSingleton);
        System.out.println(enumSingleton == newEnumSingleton);

​ 输出结果:INSTANCE
​ INSTANCE
​ true

​ 在readObject0这个函数中,对于类型是ENUM的调用了readEnum函数

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));
                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

3

​ 通过readString获取枚举对象的name,反射获取枚举类,在进行赋值并返回,因为枚举类型的name是唯一的,对应一个枚举常量,所以拿到的en肯定是唯一的。

    private Enum<?> readEnum(boolean unshared) throws IOException {
        ...
        // 获取枚举对象的名称
        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                // 赋值
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }
        ...
        return result;
    }
枚举式单例的反射
        // 枚举类单例的反射
        Class enumSingletonClass = EnumSingleton.class;
        // 枚举类型没有无参构造
        Constructor constructor = 			                                                             enumSingletonClass.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSingleton instance = (EnumSingleton) constructor.newInstance("hello", 1);
        EnumSingleton newInstance = EnumSingleton.getInstance();

        System.out.println(instance);
        System.out.println(newInstance);

​ 抛出异常: java.lang.IllegalArgumentException: Cannot reflectively create enum objects,不能用反射创建枚举对象。

​ jdk源码(1.8)中newInstance函数,c4处可以看出枚举类型使用newInstance方法会抛出异常。

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0) // c4
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
枚举单例实现

​ 那单例的实例方法可以写在INSTANCE里面,当然可以以实现接口的形式,然后也可以定义属性,比如下面代码中的data。

    public interface MySingleton {
        void doSomething();
    }	   
	public enum EnumSingleton implements MySingleton{
        INSTANCE {
            @Override
            public void doSomething() {  // 实例方法
                System.out.println("hello world");
            }
        };
        private String data; // 属性

        public String getData() {
            return data;
        }

        public void setData(String data) {
            this.data = data;
        }

        public static EnumSingleton getInstance() {
            return INSTANCE;
        }
    }
    public static void main(String[] args) {
        MySingleton instance = EnumSingleton.getInstance(); // 接口
        instance.doSomething();
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值