深入理解单例模式

前言

​ 单例模式有很多种形式:饿汉式、懒汉式、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();
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容