单例模式
单例模式
单例模式是什么
单例模式的含义是让程序中某个类运行时只存在一个对象,例如数据库连接池不会重复的创建,spring 中同一容器一个单例 bean 的生成和应用
有什么用
单例模式主要解决的问题是一个全局使用的类,不会被频繁创建和销毁,从而提升代码的整体性能。
单例模式都有哪些
饿汉式
定义
式如其名,就是人饿了,然后迫不及待地想吃东西。
饿汉式的思想就是在类加载的过程中就创建好目标对象,运行时调用目标方法去获取实例时直接返回已经实例化好的对象。
1 | public class HungrySingleton { |
缺点
JVM 只要加载了该类,不管是否有调用该类方法获取实例(如上代码中调用 getInstance() 方法),都会初始化该类创建一个实例,这样就占用了不需要的内存空间,拖慢了程序启动的时间。
懒汉式
懒汉式的定义
一如 spring 中的懒加载,不同于饿汉式,只有在调用获取实例的方法时才会去创建实例。这样就解决了饿汉式占用不必要内存空间和拖慢程序启动时间的问题了。
1 | public class LazySingleton { |
存在问题
懒汉式单例模式的代码实现看似简单,实则隐藏了两个重要问题
- 多线程环境下的问题
假如有多个线程同时访问 getInstance() 方法,可能出现 A 判断 singleton == null 为 true,然后创建实例,此时 b 也判断为 true 执行实例化,这样就会产生多个对象,违背了单例模式的初衷,因此我们需要加锁来解决这个问题。
1 | public static LazySingleton getInstance() { |
- 指令重排造成 NPE 问题
什么是指令重排序?
为了提高程序运行性能,编译器和处理器可能会对既定的代码指令执行顺序进行重新排序。
众所周知 new 一个对象的过程是分为很多步的,在多个线程同时访问该方法创建实例时可能会发生指令重排现象。
以下简单列出对象实例化的内部过程
1 | 0: new #2 //1. 加载类(如果需要);2.堆空间开辟一块内存) |
看看上一个懒汉式的代码,加入第一个线程过来了,7
执行结束,4
还未执行结束,那么切换导第二个线程由于7
执行结束了,判断 singleton == null 为 false 时,直接就返回了还未被初始化完成的 singleton 实例,所以可能会报 NullPointerException 异常
1 | /** |
以上问题其实很容易解决
只需要使用 volatile 关键字修饰共享对象就可以防止指令的重排序。
private static volatile LazySingleton singleton;
这样的懒汉式代码才算是真正安全的了。
静态内部类实现单例
由于 JVM 的类加载是懒加载,一个类只有真正被用到时才会去加载,如此,利用这个特性我们可以使用静态内部类的方法实现懒汉式。
1 | public class InnerClassSingleton { |
这种方式不需要使用synchronized
关键字即可实现懒汉式的线程安全,在开发中也比较推荐使用。
值得一提的是:在类加载器的loadClass()
方法源码中还是使用了synchronized
来实现线程安全
用枚举构造实现单例
以上的三种方式在 java 中通过反射可以直接破坏单例
以静态内部类实现方式为例
1 | public static void main(String[] args) throws Exception { |
上述代码可以破坏单例的实现,为了防止应用程序被有心人恶意植入代码,我们还需要做以下的改进:通过枚举构造来实现单例。
在反射过程中,如果通过反射创建的对象是枚举类型的,会直接抛出异常,不允许通过反射来创建枚举对象。
观察Constructor
类中的newInstance(Object... args)
方法源码:
1 | if ((clazz.getModifiers() & Modifier.ENUM) != 0) |
此处的if
判断如果你当前使用反射创建的对象是枚举类型的。就会直接抛出异常。
1 | Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class); |
- 枚举实现单例的局限
枚举类型本身隐继承了 Enum 类,又因为 java 不支持多继承,所以在存在继承的场景下这种方法不适用。而且枚举也是在类加载阶段完成对象的创建,与饿汉式类似。