Java 中的单例设计模式,很多时候我们只会注意到线程引起的表象性问题,但是没考虑过对反射机制的限制,此文旨在简单介绍利用枚举来防止反射的漏洞。
我们先展示一段最常见的懒汉式的单例:
public class Singleton { private Singleton(){} // 私有构造 private static Singleton instance = null; // 私有单例对象 // 静态工厂 public static Singleton getInstance(){ if (instance == null) { // 双重检测机制 synchronized (Singleton.class) { // 同步锁 if (instance == null) { // 双重检测机制 instance = new Singleton(); } } } return instance; } } 复制代码
上述单例的写法采用的 双重检查机制 增加了一定的安全性,但是没有考虑到 JVM 编译器的指令重排 。
比如 java 中简单的一句 instance = new Singleton,会被编译器编译成如下 JVM 指令:
memory =allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance =memory; //3:设置instance指向刚分配的内存地址 复制代码
但是这些指令顺序并非一成不变,有可能会经过 JVM 和 CPU 的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间 instance =memory; //3:设置instance指向刚分配的内存地址 ctorInstance(memory); //2:初始化对象 复制代码
对应到上文的单例模式,会产生如下图的问题:
当线程 A 执行完1,3,时,准备走2,即 instance 对象还未完成初始化,但已经不再指向 null 。
此时如果线程 B 抢占到CPU资源,执行 if(instance == null)的结果会是 false,
如何去防止呢,很简单,可以利用关键字 volatile 来修饰 instance 对象,如下图进行优化:
why?
很简单,volatile 修饰符在此处的作用就是阻止变量访问前后的指令重排,从而保证了指令的执行顺序。
意思就是,指令的执行顺序是严格按照上文的 1、2、3 来执行的,从而对象不会出现中间态。
其实,volatile 关键字在多线程的开发中应用很广,暂不赘述。
虽然很赞,但是此处仍然 没有考虑过反射机制带来的影响 。
实现单例有很多种模式,在此介绍一种使用静态内部类实现单例模式的方式:
public class Singleton { private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static Singleton getInstance() { return LazyHolder.INSTANCE; } } 复制代码
这是一种很巧妙的方式,原由是:
从外部无法访问静态内部类 LazyHolder,只有当调用 Singleton.getInstance() 方法的时候,才能得到单例对象 INSTANCE。
INSTANCE 对象初始化的时机并不是在单例类 Singleton 被加载的时候,而是在调用 getInstance 方法,使得静态内部类 LazyHolder 被加载的时候。
因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
很多种单例的写法都有一个通病,就是无法防止反射机制的漏洞,从而无法保证对象的唯一性,如下举例:
利用如下的反正代码对上文构造的单例进行对象的创建。
public static void main(String[] args) { try { //获得构造器 Constructor con = Singleton.class.getDeclaredConstructor(); //设置为可访问 con.setAccessible(true); //构造两个不同的对象 Singleton singleton1 = (Singleton)con.newInstance(); Singleton singleton2 = (Singleton)con.newInstance(); //验证是否是不同对象 System.out.println(singleton1); System.out.println(singleton2); System.out.println(singleton1.equals(singleton2)); } catch (Exception e) { e.printStackTrace(); } } 复制代码
我们直接看结果:
结果很明显,这显然是两个对象。
使用 枚举 来实现单例模式。
实现很简单,就三行代码:
public enum Singleton { INSTANCE; } 复制代码
上面所展示的就是一个单例,
why?
其实这就是 enum 的一块语法糖, JVM 会阻止反射获取枚举类的私有构造方法 。
仍然使用上文的反射代码来进行测试,发现,报错。嘿嘿,完美解决反射的问题。