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

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

小雨计算机专业毕业三年,对设计模式略有涉猎,能写一些简单的实现,掌握一些基本的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关键字的语义

保证变量对所有线程的可见性。对变量写值的时候JMM(Java内存模型)会将当前线程的工作内存值刷新到主内存,读的时候JMM会从主内存读取变量的值而不是从工作内存读取,确保一个变量值被一个线程更新后,另一个线程能立即读取到更新后的值。

禁止指令重排序优化。JVM在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,使用 volatile 可以禁止进行指令重排序优化。

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

分配内存

初始化构造器

将对象引用指向分配的内存地址

如果一个线程在实例化时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; } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wspwxz.html