java单例模式 一探到底

66 阅读3分钟

我今天想要分享的是单例模式的安全性问题。

首先单例模式是可以被反射和序列化破坏的
第一、说明为什么会被破坏
第二、讲解如何避免这些问题
第三、什么是最安全的单例模式,以及为什么最安全

以下是懒汉单例的代码,我想记录一下为什么对象要被volatile修饰。

package com.liyl.study.design;

public class LazySingleton {

    private static volatile LazySingleton lazySingleton = null;

    private LazySingleton() { }

    public static LazySingleton getInstance() {

        if(lazySingleton == null) {
            synchronized(LazySingleton.class) {
                if(lazySingleton == null) {
                    /**
                     *  对象初始化分3个指令
                     *  1、分配内存给对象
                     *  2、初始化对象
                     *  3、将对象的引用指向lazySingleton,只要执行了这一步lazySingleton就不为null
                     */
                    lazySingleton = new LazySingleton();
                }
            }
        }

        return lazySingleton;
    }
}

正如我之前写过的一篇JMM的文章,提到了指令乱序执行优化有指令重排。
那么一个对象的赋值初始化是分3个指令的,并不是原子的。

1、分配内存给对象
2、初始化对象
3、将对象的引用指向lazySingleton,只要执行了这一步lazySingleton就不为null

那么第2和3条指令重排序并不影响结果,所以是可能会被打乱执行的。

如下图,假如线程0先执行了第3条指令,还没执行第2条指令,即对象并未真正初始化,但此时如果有线程1走到了 if(lazySingleton == null) 判断,因为线程0已经执行了第3指令,那么lazySingleton 就已经不为bull,不会进入if语句进行初始化过程,返回一个还未初始化的lazySingleton对象。那么一个未初始化的对象是一定不能使用的。

使用volatile修饰的对象,可以禁止重排序1、2、3就不会被重排序,lazySingleton也就可以正常初始化。 image.png

二、序列化破坏案例

如下,恶汉单例序列化和反序列化后生成了新的对象,这个怎么应对呢。

public class Test {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        HungrySingleton hungrySingleton = HungrySingleton.getInstance();

        ObjectOutputStream oop = new ObjectOutputStream(new FileOutputStream("d://singleton"));
        oop.writeObject(hungrySingleton);

        File file = new File("d://singleton");

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton hungrySingletonObject = (HungrySingleton) ois.readObject();

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

        System.out.println("hungrySingleton == hungrySingletonObject:" + (hungrySingleton == hungrySingletonObject));
    }
}

image.png

解决方式:
如下图,在恶汉单例的类中定义一个readResolve方法,返回该实例。

image.png

那么为什么要这么做呢。让我们去看看序列化方法ObjectInputStream的readObject方法的源码。

第一: image.png 第二:进入方法后找到类型判断的switch,我们序列化的对象当然是Object子类,所以会进入readOrdinaryObject方法

image.png

三:如下图,但类默认实现了Serializable接口,desc.isInstantiable()会返回true,从而调用desc.newInstance()生成新的实例对象,那么序列化生成的对象和我们new的对象实例自然不是一个了,单例被破坏。

image.png

四:如图,到这里会判断我们的类中是否有个readResolve的方法,并且通过反射调用该方法,因为我们定义的该方法返回了单例对象本身,所以反射调用后返回的是HungrySingleton中初始化好的单例对象,代替desc.newInstance()初始化的新的对象,这样反序列化最终还是同一个单例对象。

image.png

四、最安全的单例实现:Enum

public enum  EnumSingleton implements Serializable {
    HOLDER;

    // 持有单例对象属性
    private Object instance;

    EnumSingleton(){
        instance = new Object();
    }


    public static EnumSingleton getInstance() {
        return HOLDER;
    }
}

最安全的单例模式还是通过枚举类返回,为什么呢?还是来看源码。

通过反射调用产生实例的方法会报错,是因为enum没有无参构造,只有一个有参构造函数,如下图

image.png

那么我们就反射调用它的有参构造,会抛出该异常:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects

查看Constructor的newInstance方法可知,如果反射的是枚举类型直接抛出该异常,不允许反射调用。直接给抛出异常。如下图

image.png