面试官:请写一个你认为比较“完美”的单例

单例模式是保证一个类的实例有且只有一个,在须要控制资源(如数据库链接池),或资源共享(若有状态的工具类)的场景中比较适用。若是让咱们写一个单例实现,估计绝大部分人都以为本身没问题,但若是须要实现一个比较完美的单例,可能并无你想象中简单。本文以主人公小雨的一次面试为背景,按部就班地讨论如何实现一个较为“完美”的单例。本文人物与场景皆为虚构,若有雷同,纯属捏造。java

小雨计算机专业毕业三年,对设计模式略有涉猎,能写一些简单的实现,掌握一些基本的JVM知识。在某次面试中,面试官要求现场写代码:请写一个你认为比较“完美”的单例。面试

简单的单例实现

凭借着对单例的理解与印象,小雨写出了下面的代码数据库

public class Singleton {
    private static Singleton instance;

    private Singleton(){}

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

写完后小雨审视了一遍,总以为有点太简单了,离“完美”貌似还相差甚远。对,在多线程并发环境下,这个实现就玩不转了,若是两个线程同时调用 getInstance() 方法,同时执行到了 if 判断,则两边都认为 instance 实例为空,都会实例化一个 Singleton 对象,就会致使至少产生两个实例了,小雨心想。嗯,须要解决多线程并发环境下的同步问题,保证单例的线程安全。设计模式

线程安全的单例

一提到并发同步问题,小雨就想到了锁。加个锁还不简单,synchronized 搞起,安全

public class Singleton {
    private static Singleton instance;

    private Singleton(){}

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

小雨再次审视了一遍,发现貌似每次 getInstance() 被调用时,其它线程必须等待这个线程调用完才能执行(由于有锁锁住了嘛),可是加锁实际上是想避免多个线程同时执行实例化操做致使产生多个实例,在单例被实例化后,后续调用 getInstance() 直接返回就好了,每次都加锁释放锁形成了没必要要的开销。多线程

通过一阵思索与回想以后,小雨记起了曾经看过一个叫 Double-Checked Locking 的东东,双重检查锁,嗯,再优化一下,并发

public class Singleton {
    private static volatile Singleton instance;

    private Singleton(){}

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

单例在完成第一次实例化,后续再调用 getInstance() 先判空,若是不为空则直接返回,若是为空,就算两个线程同时判断为空,在同步块中还作了一次双重检查,能够确保只会实例化一次,省去了没必要要的加锁开销,同时也保证了线程安全。而且令小雨感到自我知足的是他基于对JVM的一些了解加上了 volatile 关键字来避免实例化时因为指令重排序优化可能致使的问题,真是画龙点睛之笔啊。 简直——完美!函数

Tips: volatile关键字的语义工具

  1. 保证变量对全部线程的可见性。对变量写值的时候JMM(Java内存模型)会将当前线程的工做内存值刷新到主内存,读的时候JMM会从主内存读取变量的值而不是从工做内存读取,确保一个变量值被一个线程更新后,另外一个线程能当即读取到更新后的值。
  2. 禁止指令重排序优化。JVM在执行程序时为了提升性能,编译器和处理器经常会对指令作重排序,使用 volatile 能够禁止进行指令重排序优化。

JVM建立一个新的实例时,主要需三步:性能

  1. 分配内存
  2. 初始化构造器
  3. 将对象引用指向分配的内存地址

若是一个线程在实例化时JVM作了指令重排,好比先执行了1,再执行3,最后执行2,则另外一个线程可能获取到一个尚未完成初始化的对象引用,调用时可能致使问题,使用volatile能够禁止指令重排,避免这种问题。

小雨将答案交给面试官,面试官瞄了一眼说道:“基本可用了,但若是我用反射直接调用这个类的构造函数,是否是就不能保证单例了。” 小雨挠挠头,对哦,若是使用反射就能够在运行时改变单例构造器的可见性,直接调用构造器来建立一个新的实例了,好比经过下面这段代码

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
 constructor.setAccessible(true);
 Singleton singleton = constructor.newInstance();

小雨再次陷入了思考。

反射安全的单例

怎么避免反射破坏单例呢,或许能够加一个静态变量来控制,让构造器只有从 getInstance() 内部调用才有效,不经过 getInstance() 直接调用则抛出异常,小雨按这个思路作了一番改造,

public class Singleton {
    private static volatile Singleton instance;
    private static boolean flag = false;

    private Singleton(){
        synchronized (Singleton.class) {
            if (flag) {
                flag = false;
            } else {
                throw new RuntimeException("Please use getInstance() method to get the single instance.");
            }
        }

    }

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

使用静态变量 flag 来控制,只有从 getInstance() 调用构造器才能正常实例化,不然抛出异常。但立刻小雨就发现了存在的问题:既然能够经过反射来调用构造器,那么也能够经过反射来改变 flag 的值,这样苦心设置的 flag 控制逻辑不就被打破了吗。看来也没那么“完美”。虽然并不那么完美,但也必定程度上规避了使用反射直接调用构造器的场景,而且貌似也想不出更好的办法了,因而小雨提交了答案。

面试官露出迷之微笑:“想法挺好,反射的问题基本解决了,但若是我序列化这个单例对象,而后再反序列化出来一个对象,这两个对象还同样吗,还能保证单例吗。若是不能,怎么解决这个问题?”

SerializationSafeSingleton s1 = SerializationSafeSingleton.getInstance();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(s1);
oos.close();

ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
SerializationSafeSingleton s2 = (SerializationSafeSingleton) ois.readObject();
ois.close();

s1 == s2 吗? 答案是否,如何解决呢。

序列化安全的单例

小雨思考了一会,想起了曾经学习序列化知识时接触的 readResolve() 方法,该方法在ObjectInputStream已经读取一个对象并在准备返回前调用,能够用来控制反序列化时直接返回一个对象,替换从流中读取的对象,因而在前面实现的基础上,小雨添加了一个 readResolve() 方法,

public class Singleton {
    private static volatile Singleton instance;
    private static boolean flag = false;

    private Singleton(){
        synchronized (Singleton.class) {
            if (flag) {
                flag = false;
            } else {
                throw new RuntimeException("Please use getInstance() method to get the single instance.");
            }
        }

    }

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

    /**
     * 该方法代替了从流中读取对象
     * @return
     */
    private Object readResolve(){
        return getInstance();
    }
}

经过几个步骤的逐步改造优化,小雨完成了一个基本具有线程安全、反射安全、序列化安全的单例实现,心想这下应该足够完美了吧。面试官脸上继续保持着迷之微笑:“这个实现看起来仍是显得有点复杂,而且也不能彻底解决反射安全的问题,想一想看还有其它实现方案吗。”

其它方案

小雨反复思考,前面的实现是经过加锁来实现线程安全,除此以外,还能够经过类的加载机制来实现线程安全——类的静态属性只会在第一次加载类时初始化,而且在初始化的过程当中,JVM是不容许其它线程来访问的,因而又写出了下面两个版本

1.静态初始化版本

public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton(){}

    public static final Singleton getInstance() {
        return instance;
    }
}

该版本借助JVM的类加载机制,自己线程安全,但只要 Singleton 类的某个静态对象(方法或属性)被访问,就会形成实例的初始化,而该实例可能根本不会被用到,形成资源浪费,另外一方面也存在反射与序列化的安全性问题,也须要进行相应的处理。

2.静态内部类版本

public class Singleton {
    private Singleton(){}

    public static final Singleton getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
}

该版本只有在调用 getInstance() 才会进行实例化,即延迟加载,避免资源浪费的问题,同时也能保障线程安全,可是一样存在反射与序列化的安全性问题,须要相应处理。

这貌似跟前面版本的复杂性差很少啊,依然都须要解决反射与安全性的问题,小雨心想,有没有一种既简单又能避免这些问题的方案呢。

“完美”方案

一阵苦思冥想以后,小雨忽然脑中灵光闪现,枚举!(这也是《Effective Java》的做者推荐的方式啊)

public enum Singleton {
    INSTANCE;

    public void func(){
        ...
    }
}

能够直接经过 Singleton.INSTANCE 来引用单例,很是简单的实现,而且既是线程安全的,同时也能应对反射与序列化的问题,面试官想要的估计就是它了吧。小雨再次提交了答案,这一次,面试官脸上的迷之微笑逐渐消失了……

Tips:为何枚举是线程、反射、序列化安全的?

  1. 枚举实际是经过一个继承自Enum的final类来实现(经过反编译class文件可看到具体实现),在static代码块中对其成员进行初始化,所以借助类加载机制来保障其线程安全
  2. 枚举是不支持经过反射实例化的,在Constructor类的newInstance方法中可看到
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");
  1. 枚举在序列化的时候仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是经过java.lang.Enum的valueOf方法来根据名字查找枚举对象。而且,编译器是不容许任何对这种序列化机制的定制的,禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。枚举经过这种机制保障了序列化安全。

总结

枚举方案近乎“完美”,但实际中,大部分状况下,咱们使用双重检查锁方案或静态内部类方案基本都能知足咱们的场景并能很好地运行。而且方案历来没有“完美”,只有更好或更合适。本文只是从单例实现的不断演进的过程当中,了解或回顾如反射、序列化、线程安全、Java内存模型(volatile语义)、JVM类加载机制、JVM指令重排序优化等方面的知识,同时也是启示咱们在设计或实现的过程当中,多从各个角度思考,尽量全面地考虑问题。或者,在相关面试中能更好地迎合面试官的“完美”指望。


做者:雨歌,一枚仍在学习路上的IT老兵
欢迎关注做者公众号:半路雨歌,一块儿学习成长
qrcode

相关文章
相关标签/搜索