5

这9个单例被破坏的事故现场,你遇到过几个? 评论区见

 2 years ago
source link: https://www.cnblogs.com/gupaoedu-tom/p/15465795.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1 通用单例写法带来的弊端

我们看到的单例模式通用写法,一般就是饿汉式单例的标准写法。饿汉式单例写法在类加载的时候立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现之前就实例化了,不可能存在访问安全问题。饿汉式单例还有另外一种写法,代码如下。


//饿汉式静态代码块单例模式
public class HungryStaticSingleton {
    private static final HungryStaticSingleton instance;
    
    static {
        instance = new HungryStaticSingleton();
    }

    private HungryStaticSingleton(){}

    public static HungryStaticSingleton getInstance(){
        return  instance;
    }
}

这种写法使用静态代码块的机制,非常简单也容易理解。饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全,执行效率比较高。但是它的缺点也很明显,就是所有对象类在加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,而且单例对象的数量也不确定,则系统初始化时会造成大量的内存浪费,从而导致系统内存不可控。也就是说,不管对象用或不用,都占着空间,浪费了内存,有可能占着内存又不使用。那有没有更优的写法呢?我们继续分析。

2 还原线程破坏单例的事故现场

为了解决饿汉式单例写法可能带来的内存浪费问题,于是出现了懒汉式单例的写法。懒汉式单例写法的特点是单例对象在被使用时才会初始化。懒汉式单例写法的简单实现LazySimpleSingleton如下。


//懒汉式单例模式在外部需要使用的时候才进行实例化
public class LazySimpleSingletion {
    //静态块,公共内存区域
    private static LazySimpleSingletion instance;

    private LazySimpleSingletion(){}

    public  static LazySimpleSingletion getInstance(){
        if(instance == null){
            instance = new LazySimpleSingletion();
        }
        return instance;
    }
}

但这样写又带来了一个新的问题,如果在多线程环境下,则会出现线程安全问题。先来模拟一下,编写线程类ExectorThread。

public class ExectorThread implements Runnable{
    @Override
    public void run() {
        LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + singleton);
    }
}

编写客户端测试代码如下。

public class LazySimpleSingletonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }
}

我们反复多次运行程序上的代码,发现会有一定概率出现两种不同结果,有可能两个线程获取的对象是一致的,也有可能两个线程获取的对象是不一致的。下图是两个线程获取的对象不一致的运行结果。

file

下图是两个线程获取的对象一致的结果。

file

显然,这意味着上面的单例存在线程安全隐患。那么这个结果是怎么产生的呢?我们来分析一下,如下图所示,如果两个线程在同一时间同时进入getInstance()方法,则会同时满足if(null == instance)条件,创建两个对象。如果两个线程都继续往下执行后面的代码,则有可能后执行的线程的结果覆盖先执行的线程的结果。如果打印动作发生在覆盖之前,则最终得到的结果就是一致的;如果打印动作发生在覆盖之后,则得到两个不一样的结果。

file

当然,也有可能没有发生并发,完全正常运行。下面通过调试方式来更深刻地理解一下。这里教大家一种新技能,用线程模式调试,手动控制线程的执行顺序来跟踪内存的变化。先把ExectorThread类打上断点,如下图所示。

file

单击右键点击断点,切换为Thread模式,如下图所示。

file

然后把LazySimpleSingleton类也打上断点,同样标记为Thread模式,如下图所示。

file

切换回客户端测试代码,同样也打上断点,同时改为Thread模式,如下图所示。

file

在开始Debug之后,我们会看到Debug控制台可以自由切换Thread的运行状态,如下图所示。

file

通过不断切换线程,并观测其内存状态,我们发现在线程环境下LazySimpleSingleton被实例化了两次。有时候得到的运行结果可能是两个相同的对象,实际上是被后面执行的线程覆盖了,我们看到了一个假象,线程安全隐患依旧存在。那么,如何优化代码,使得懒汉式单例模式在线程环境下安全呢?来看下面的代码,给getInstance()方法加上synchronized关键字,使这个方法变成线程同步方法。


public class LazySimpleSingletion {
    //静态块,公共内存区域
    private static LazySimpleSingletion instance;

    private LazySimpleSingletion(){}

    public synchronized static LazySimpleSingletion getInstance(){
        if(instance == null){
            instance = new LazySimpleSingletion();
        }
        return instance;
    }
}

我们再来调试。当执行其中一个线程并调用getInstance()方法时,另一个线程在调用getInstance()方法,线程的状态由RUNNING变成了MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复到RUNNING状态继续调用getInstance()方法,如下图所示。

file

这样,通过使用synchronized就解决了线程安全问题。

3 双重检查锁单例写法闪亮登场

在上一节中,我们通过调试的方式完美地展现了synchronized监视锁的运行状态。但是,如果在线程数量剧增的情况下,用synchronized加锁,则会导致大批线程阻塞,从而导致程序性能大幅下降。就好比是地铁进站限流,在寒风刺骨的冬天,所有人都在站前广场转圈圈,用户体验很不好,如下图所示。

file

那有没有办法优化一下用户体验呢?其实可以让所有人先进入进站大厅,然后增设一些进站闸口,这样用户体验变好了,进站效率也提高了。当然,在现实生活中可能会受到很多硬性条件的限制,但是在虚拟世界中是完全可以实现的。其实这就叫作双重检查,在进站门安检一次,进入大厅后在闸口检票处再检查一次,如下图所示。

file

我们来改造一下代码,创建LazyDoubleCheckSingleton类。


public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton instance;
    private LazyDoubleCheckSingleton(){}

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

这样写就解决问题了吗?目测发现,其实这跟LazySimpleSingletion的写法并无差异,还是会大规模阻塞。那我们把判断条件往上提一级呢?


public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton instance;
    private LazyDoubleCheckSingleton(){}

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

在运行代码后,还是会存在线程安全问题。运行结果如下图所示。

这是什么原因导致的呢?其实如果两个线程在同一时间都满足if(instance == null)条件,则两个线程都会执行synchronized块中的代码,因此,还是会创建两次。再优化一下代码。


public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton instance;
    private LazyDoubleCheckSingleton(){}

    public static LazyDoubleCheckSingleton getInstance(){
        //检查是否要阻塞
        if (instance == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                //检查是否要重新创建实例
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                    //指令重排序的问题
                }
            }
        }
        return instance;
    }
}

我们进行断点调试,如下图所示。

当第一个线程调用getInstance()方法时,第二个线程也可以调用。当第一个线程执行到synchronized时会上锁,第二个线程就会变成MONITOR状态,出现阻塞。此时,阻塞并不是基于整个LazyDoubleCheckSingleton类的阻塞,而是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感觉不到。

4 看似完美的静态内部类单例写法

双重检查锁单例写法虽然解决了线程安全问题和性能问题,但是只要用到synchronized关键字总是要上锁,对程序性能还是存在一定影响的。难道就真的没有更好的方案吗?当然有。我们可以从类初始化的角度考虑,看下面的代码,采用静态内部类的方式。


//这种形式兼顾饿汉式单例写法的内存浪费问题和synchronized的性能问题
//完美地屏蔽了这两个缺点
public class LazyStaticInnerClassSingleton {
    //使用LazyInnerClassGeneral的时候,默认会先初始化内部类
    //如果没使用,则内部类是不加载的
    private LazyStaticInnerClassSingleton(){

    }
    //每一个关键字都不是多余的,static是为了使单例的空间共享,保证这个方法不会被重写、重载
    private static LazyStaticInnerClassSingleton getInstance(){
        //在返回结果之前,一定会先加载内部类
        return LazyHolder.INSTANCE;
    }

    //利用了Java本身的语法特点,默认不加载内部类
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }

}

这种方式兼顾了饿汉式单例写法的内存浪费问题和synchronized的性能问题。内部类一定要在方法调用之前被初始化,巧妙地避免了线程安全问题。由于这种方式比较简单,就不再一步步调试。但是,“金无足赤,人无完人”,单例模式亦如此。这种写法就真的完美了吗?

5 还原反射破坏单例的事故现场

我们来看一个事故现场。大家有没有发现,上面介绍的单例模式的构造方法除了加上private关键字,没有做任何处理。如果使用反射来调用其构造方法,再调用getInstance()方法,应该有两个不同的实例。现在来看客户端测试代码,以LazyStaticInnerClassSingleton为例。


    public static void main(String[] args) {
        try{
            //如果有人恶意用反射破坏
            Class<?> clazz = LazyStaticInnerClassSingleton.class;

            //通过反射获取私有的构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //强制访问
            c.setAccessible(true);

            //暴力初始化
            Object o1 = c.newInstance();
            
            //调用了两次构造方法,相当于“new”了两次,犯了原则性错误
            Object o2 = c.newInstance();

            System.out.println(o1 == o2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
		

运行结果如下图所示。

显然,内存中创建了两个不同的实例。那怎么办呢?我们来做一次优化。我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。优化后的代码如下。


public class LazyStaticInnerClassSingleton {
    //使用LazyInnerClassGeneral的时候,默认会先初始化内部类
    //如果没使用,则内部类是不加载的
    private LazyStaticInnerClassSingleton(){
        if(LazyHolder.INSTANCE != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }
    //每一个关键字都不是多余的,static是为了使单例的空间共享,保证这个方法不会被重写、重载
    private static LazyStaticInnerClassSingleton getInstance(){
        //在返回结果之前,一定会先加载内部类
        return LazyHolder.INSTANCE;
    }

    //利用了Java本身的语法特点,默认不加载内部类
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }

}

再运行客户端测试代码,结果如下图所示。

至此,自认为最优雅的单例模式写法便大功告成了。但是,上面看似完美的单例写法还是值得斟酌的。在构造方法中抛出异常,显然不够优雅。那么有没有比静态内部类更优雅的单例写法呢?

6 更加优雅的枚举式单例写法问世

枚举式单例写法可以解决上面的问题。首先来看枚举式单例的标准写法,创建EnumSingleton类。


public enum EnumSingleton {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

然后看客户端测试代码。


public class EnumSingletonTest {
    public static void main(String[] args) {
        try {
            EnumSingleton instance1 = null;

            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setData(new Object());

            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance1 = (EnumSingleton) ois.readObject();
            ois.close();

            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData() == instance2.getData());

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

最后得到运行结果,如下图所示。

我们没有对代码逻辑做任何处理,但运行结果和预期一样。那么枚举式单例写法如此神奇,它的神秘之处体现在哪里呢?下面通过分析源码来揭开它的神秘面纱。
首先下载一个非常好用的Java反编译工具Jad,在解压后配置好环境变量(这里不做详细介绍),就可以使用命令行调用了。找到工程所在的Class目录,复制EnumSingleton.class所在的路径,如下图所示。

然后切换到命令行,切换到工程所在的Class目录,输入命令jad并输入复制好的路径,在Class目录下会多出一个EnumSingleton.jad文件。打开EnumSingleton.jad文件,我们惊奇地发现有如下代码。


static 
{
    INSTANCE = new EnumSingleton("INSTANCE", 0);
    $VALUES = (new EnumSingleton[] {
        INSTANCE
    });
}

原来,枚举式单例写法在静态代码块中就对INSTANCE进行了赋值,是饿汉式单例写法的实现。至此,我们还可以试想,序列化能否破坏枚举式单例写法呢?不妨再来看一下JDK源码,还是回到ObjectInputStream的readObject0()方法。


private Object readObject0(boolean unshared) throws IOException {
            ...

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

            ...
}

我们看到,在readObject0()中调用了readEnum()方法,readEnum()方法的代码实现如下。


private Enum<?> readEnum(boolean unshared) throws IOException {
    if (bin.readByte() != TC_ENUM) {
        throw new InternalError();
    }

    ObjectStreamClass desc = readClassDesc(false);
    if (!desc.isEnum()) {
        throw new InvalidClassException("non-enum class: " + desc);
    }

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

    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);
        }
    }

    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

由上可知,枚举类型其实通过类名和类对象找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。那么反射是否能破坏枚举式单例写法的单例对象呢?来看客户端测试代码。


public static void main(String[] args) {
    try {
        Class clazz = EnumSingleton.class;
        Constructor c = clazz.getDeclaredConstructor();
        c.newInstance();
    }catch (Exception e){
        e.printStackTrace();
    }
}

运行结果如下图所示。

结果中报出的是java.lang.NoSuchMethodException异常,意思是没找到无参的构造方法。此时,打开java.lang.Enum的源码,查看它的构造方法,只有一个protected类型的构造方法,代码如下。


protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

再来做一个这样的测试。


public static void main(String[] args) {
    try {
        Class clazz = EnumSingleton.class;
        Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
        c.setAccessible(true);
        EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("Tom",666);

    }catch (Exception e){
        e.printStackTrace();
    }
}

运行结果如下图所示。

这时,错误已经非常明显了,“Cannot reflectively create enum objects”,即不能用反射来创建枚举类型。我们还是习惯性地想来看下JDK源码,进入Constructor的newInstance()方法。


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)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor; 
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

从上述代码可以看到,在newInstance()方法中做了强制性的判断,如果修饰符是Modifier.ENUM枚举类型,则直接抛出异常。这岂不是和静态内部类单例写法的处理方式有异曲同工之妙?对,但是我们在构造方法中写逻辑处理可能存在未知的风险,而JDK的处理是最官方、最权威、最稳定的。因此,枚举式单例写法也是Effective Java一书中推荐的一种单例模式写法。
到此为止,我们是不是已经非常清晰明了呢?JDK枚举的语法特殊性及反射也为枚举保驾护航,让枚举式单例写法成为一种更加优雅的实现。

7 还原反序列化破坏单例的事故现场

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,当下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,则违背了单例模式的初衷,相当于破坏了单例,来看一段代码。


//反序列化破坏了单例模式
public class SeriableSingleton implements Serializable {
    //序列化就是把内存中的状态通过转换成字节码的形式
    //从而转换为一个I/O流,写入其他地方(可以是磁盘、网络I/O)
    //内存中的状态会被永久保存下来

    //反序列化就是将已经持久化的字节码内容转换为I/O流
    //通过I/O流的读取,进而将读取的内容转换为Java对象
    //在转换过程中会重新创建对象
    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }
}

编写客户端测试代码。


    public static void main(String[] args) {
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
		

运行结果如下图所示。

从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,被实例化了两次,违背了单例模式的设计初衷。那么,如何保证在序列化的情况下也能够实现单例模式呢?其实很简单,只需要增加readResolve()方法即可。优化后的代码如下。


public class SeriableSingleton implements Serializable {

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }
    private  Object readResolve(){
        return  INSTANCE;
    }
}

再看运行结果,如下图所示。

大家一定会想:这是什么原因呢?为什么要这样写?看上去很神奇的样子,也让人有些费解。不如一起来看JDK的源码实现以了解清楚。进入ObjectInputStream类的readObject()方法,代码如下。


public final Object readObject()
    throws IOException, ClassNotFoundException
{
    if (enableOverride) {
        return readObjectOverride();
    }

    int outerHandle = passHandle;
    try {
        Object obj = readObject0(false);
        handles.markDependency(outerHandle, passHandle);
        ClassNotFoundException ex = handles.lookupException(passHandle);
        if (ex != null) {
            throw ex;
        }
        if (depth == 0) {
            vlist.doCallbacks();
        }
        return obj;
    } finally {
        passHandle = outerHandle;
        if (closed && depth == 0) {
            clear();
        }
    }
}

可以看到,在readObject()方法中又调用了重写的readObject0()方法。进入readObject0()方法,源码如下。


private Object readObject0(boolean unshared) throws IOException {
            ...

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

            ...
}

我们看到TC_OBJECT中调用了ObjectInputStream的readOrdinaryObject()方法,源码如下。


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);
    }

    ...

    return obj;
}

我们发现调用了ObjectStreamClass的isInstantiable()方法,而isInstantiable()方法的源码如下。


boolean isInstantiable() {
    requireInitialized();
    return (cons != null);
}

上述代码非常简单,就是判断一下构造方法是否为空。如果构造方法不为空,则返回true。这意味着只要有无参构造方法就会实例化。
这时候其实还没有找到加上readResolve()方法就可以避免单例模式被破坏的真正原因。再回到ObjectInputStream的readOrdinaryObject()方法,继续往下看源码。


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);
    }

    ...

    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) {
            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;
}

在判断无参构造方法是否存在之后,又调用了hasReadResolveMethod()方法,源码如下。


boolean hasReadResolveMethod() {
    requireInitialized();
    return (readResolveMethod != null);
}

上述代码的逻辑非常简单,就是判断readResolveMethod是否为空,如果不为空,则返回true。那么readResolveMethod是在哪里被赋值的呢?通过全局查找知道,在私有方法ObjectStreamClass()中对readResolveMethod进行了赋值,源码如下。


readResolveMethod = getInheritableMethod(
    cl, "readResolve", null, Object.class);
		

上面的逻辑其实就是通过反射找到一个无参的readResolve()方法,并且保存下来。再回到ObjectInputStream的readOrdinaryObject()方法,继续往下看,如果readResolve()方法存在,则调用invokeReadResolve()方法,代码如下。


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); 
            }
        } catch (IllegalAccessException ex) {
            throw new InternalError(ex);
        }
    } else {
        throw new UnsupportedOperationException();
    }
}

可以看到,在invokeReadResolve()方法中用反射调用了readResolveMethod方法。
通过JDK源码分析可以看出,虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上单例对象被实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率加快,则意味着内存分配开销也会随之增大,难道真的就没办法从根本上解决问题吗?其实,枚举式单例写法也是能够避免这个问题发生的,因为它在类加载的时候就已经创建好了所有的对象。

8 还原克隆破坏单例的事故现场

假设有这样一个场景,如果克隆的目标对象恰好是单例对象,那会不会使单例对象被破坏呢?当然,我们在已知的情况下肯定不会这么干,但如果发生了意外怎么办?不妨来修改一下代码。


@Data
public class ConcretePrototype implements Cloneable {

    private static  ConcretePrototype instance = new ConcretePrototype();

    private ConcretePrototype(){}

    public static ConcretePrototype getInstance(){
        return instance;
    }

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    
}

我们把构造方法私有化,并且提供getInstance()方法。编写客户端测试代码如下。


    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype prototype = ConcretePrototype.getInstance();

        //复制原型对象
        ConcretePrototype cloneType = prototype.clone();

        System.out.println("原型对象和克隆对象比较:" + (prototype == cloneType));

}

运行结果如下图所示。

file

从运行结果来看,确实创建了两个不同的对象。实际上防止克隆破坏单例对象的解决思路非常简单,禁止克隆便可。要么我们的单例类不实现Cloneable接口,要么我们重写clone()方法,在clone()方法中返回单例对象即可,具体代码如下。


    @Override
    public ConcretePrototype clone() {
        return instance;
}

9 容器式单例写法解决大规模生产单例的问题

虽然枚举式单例写法更加优雅,但是也会存在一些问题。因为它在类加载时将所有的对象初始化都放在类内存中,这其实和饿汉式单例写法并无差异,不适合大量创建单例对象的场景。接下来看注册式单例模式的另一种写法,即容器式单例写法,创建ContainerSingleton类。


public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    public static Object getBean(String className){
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}

容器式单例写法适用于需要大量创建单例对象的场景,便于管理,但它是非线程安全的。到此,注册式单例写法介绍完毕。再来看Spring中的容器式单例写法的源码。


public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
      implements AutowireCapableBeanFactory {
   /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */
   private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>(16);
...
}

从上面代码来看,存储单例对象的容器其实就是一个Map。

9 附彩蛋:ThreadLocal线程单例

最后赠送大家一个彩蛋,线程单例实现ThreadLocal。ThreadLocal不能保证其创建的对象是全局唯一的,但能保证在单个线程中是唯一的,是线程安全的。下面来看代码。


public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };
    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

客户端测试代码如下。


public static void main(String[] args) {

    System.out.println(ThreadLocalSingleton.getInstance());
    System.out.println(ThreadLocalSingleton.getInstance());
    System.out.println(ThreadLocalSingleton.getInstance());
    System.out.println(ThreadLocalSingleton.getInstance());
    System.out.println(ThreadLocalSingleton.getInstance());

    Thread t1 = new Thread(new ExectorThread());
    Thread t2 = new Thread(new ExectorThread());
    t1.start();
    t2.start();
    System.out.println("End");
}

运行结果如下图所示。

file

由上可知,在主线程中无论调用多少次,获取的实例都是同一个,都在两个子线程中分别获取了不同的实例。那么,ThreadLocal是如何实现这样的效果的呢?我们知道,单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLocal将所有对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。

file

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注微信公众号『 Tom弹架构 』可获取更多技术干货!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK