枚举单例
单例模式在我们书写代码中是最经常使用的一种设计模式,但是这种设计模式真的安全吗?如果不安全的话,我们有没有安全的单例模式?其实这也是大厂面试的时候可能会问道的面试题,本篇我们来研究下这个问题。
一、引出问题
- 双重锁定单例和静态内部类单例安全吗?
- 枚举单例使用过吗?它为什么是安全的?
- Kotlin 中的单例有使用过吗?
1. 双重锁定单例
首先我们快速回忆一下双重锁定单例和静态内部类单例,首先双重锁定单例如下:
public class Manager {
private volatile static Manager INSTANCE = null;
private Manager() {
}
public static Manager getInstance() {
if (INSTANCE == null) {//第一次判空
synchronized (Manager.class) {
if (INSTANCE == null) {//第二次判空
INSTANCE = new Manager();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
Manager instance = Manager.getInstance();
Manager instance2 = Manager.getInstance();
System.out.println(instance);
//com.oman.forward.pattern.create.singleton.Manager@5ca881b5
System.out.println(instance2);
//com.oman.forward.pattern.create.singleton.Manager@5ca881b5
System.out.println(instance == instance2);
//true
}
}
为什么要两次判空呢?因为假如两个线程都走到了锁的地方,第一个线程持有了锁之后,内部判断为空,创建实例成功后释放锁。此时第二个线程获取到了锁,假如没有第二个判空条件,很明显就又会再次实例化一次对象,那么所谓的单例就不是全局唯一的了。
为什么加上 volatile 关键字?主要为了避免指令排序导致的问题,因为对象的创建最少需要三个步骤:
- 在堆内存开辟内存控件;
- 内存控件的初始化零值;
- 将实例化的对象指向栈中的符号引用。
如果不加上 volatile 的话,可能会导致步骤 2 和 3 的执行顺序得不到保证,假如线程 1 先执行了步骤 1 和步骤 3,此时线程 2 在外部第一次判断不为空,就去直接使用单例对象了,就会出现问题。
2. 静态内部类单例
下面看静态内部类的方式创建的单例:
public class Manager2 {
private Manager2() {
}
private static class SingletonHolder {
private static final Manager2 INSTANCE = new Manager2();
}
public static Manager2 getInstance() {
return SingletonHolder.INSTANCE;
}
public static void main(String[] args) {
Manager2 instance = Manager2.getInstance();
Manager2 instance2 = Manager2.getInstance();
System.out.println(instance);
//com.oman.forward.pattern.create.singleton.Manager2@5ca881b5
System.out.println(instance2);
//com.oman.forward.pattern.create.singleton.Manager2@5ca881b5
System.out.println(instance == instance2);
//true
}
}
静态内部类方式创建单例也是通过懒加载的方式创建的单例,因为静态内部类不属于 JVM 规定的必须进行初始化的情况,只有在调用 getInstance 静态方法的时候才会进行内部类的初始化,保证了类的延迟加载,不浪费资源。
并且虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行 <clinit>()
方法完毕。如果在一个类的 <clinit>()
方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行 <clinit>()
方法后,其他线程唤醒之后不会再次进入 <clinit>()
方法。同一个加载器下,一个类型只会初始化一次)。
所以可以看出 INSTANCE 在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
但是静态内部类单例有一个很大的问题就是不能传递参数,如果需要传递参数的话,就不能使用这种方式创建单例了。
3. 以上两种单例是安全的吗?
下面我们通过代码来演示:
public static void main(String[] args) throws Exception {
Manager instance = Manager.getInstance();
Manager instance2 = Manager.getInstance();
System.out.println(instance);
//com.oman.forward.pattern.create.singleton.Manager@5ca881b5
System.out.println(instance2);
//com.oman.forward.pattern.create.singleton.Manager@5ca881b5
System.out.println(instance == instance2);
//true
Constructor<Manager> constructor
= Manager.class.getDeclaredConstructor();
constructor.setAccessible(true);
Manager manager = constructor.newInstance();
System.out.println(manager);
//com.oman.forward.pattern.create.singleton.Manager@24d46ca6
System.out.println(instance == manager);
//false
}
从代码可以看出,我们通过反射又可以重新创建一个单例对象的实例,并且和原来的地址值不是同一个(静态内部类的单例可以通过同一种方式验证),那这样的话单例还安全吗?
4. 枚举:安全的单例
我们使用枚举来创建单例,看看效果如何,是否安全
public enum EnumSingle {
INSTANCE;
public void test() {
System.out.println("test");
}
public static void main(String[] args) throws Exception {
EnumSingle single = EnumSingle.INSTANCE;
EnumSingle single2 = EnumSingle.INSTANCE;
System.out.println(single);//INSTANCE
System.out.println(single2);//INSTANCE
System.out.println(single == single2);//true
}
}
上面的结果并不能说明什么,我们也使用反射获取一下单例,看看效果如何?
public static void main(String[] args) throws Exception {
Constructor<EnumSingle> enumSingleConstructor = EnumSingle.class.getDeclaredConstructor();
enumSingleConstructor.setAccessible(true);
EnumSingle enumSingle = enumSingleConstructor.newInstance();
System.out.println(enumSingle);
}
上述代码在运行的时候会报错如下:
Exception in thread "main" java.lang.NoSuchMethodException: com.oman.forward.pattern.create.singleton.EnumSingle.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at com.oman.forward.pattern.create.singleton.EnumSingle.main(EnumSingle.java:13)`
在 debug 的时候发现,只有一个参数为(String.class,int.class)构造器,其实看下父类 Enum 源码就明白,这两个参数是 name 和 ordial 两个属性:
那么我们使用这两个参数的构造方法使用反射获取一下,看看效果,于是代码改成了下面这样:
public static void main(String[] args) throws Exception {
Constructor<EnumSingle> enumSingleConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
enumSingleConstructor.setAccessible(true);
EnumSingle enumSingle = enumSingleConstructor.newInstance("name",1);
System.out.println(enumSingle);
}
运行一下看看效果如何?
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at com.oman.forward.pattern.create.singleton.EnumSingle.main(EnumSingle.java:15)
从上面我看到还是会报错,不过报错的内容变为了 Cannot reflectively create enum objects
,查看源码 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;// read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
我们能看到报错是源自于下面这一行,说明了 java 虚拟机不允许对枚举进行反射。从而说明枚举是安全的单例。
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
说到这里,其实我们可以完全按照 java 虚拟机的这种思想,对我们的单例设置安全性,代码如下:
public class Manager {
private volatile static Manager INSTANCE = null;
private Manager() {
if ((this.getClass().getModifiers() & Modifier.PUBLIC) != 0) {
throw new IllegalArgumentException("Cannot reflectively create this objects");
}
}
public static Manager getInstance() {
if (INSTANCE == null) {
synchronized (Manager.class) {
if (INSTANCE == null) {
INSTANCE = new Manager();
}
}
}
return INSTANCE;
}
public static void main(String[] args) throws Exception {
Constructor<Manager> constructor = Manager.class.getDeclaredConstructor();
constructor.setAccessible(true);
Manager manager = constructor.newInstance();
}
}
这样在运行上述代码的时候,就会报错,不允许反射创建实例了,保证了单例的全局唯一并且安全性。
其实单例模式在序列化的时候也会有一些问题: 首先看单例的序列化和反序列化问题:
public class Manager implements Serializable {
private static final long serialVersionUID = 36249882076318126L;
private volatile static Manager INSTANCE = null;
private Manager() {
}
public static Manager getInstance() {
if (INSTANCE == null) {
synchronized (Manager.class) {
if (INSTANCE == null) {
INSTANCE = new Manager();
}
}
}
return INSTANCE;
}
public static void main(String[] args) throws Exception {
Manager instance = Manager.getInstance();
Manager instance2 = Manager.getInstance();
System.out.println(instance);
//com.oman.forward.pattern.create.singleton.Manager@5ca881b5
System.out.println(instance2);
//com.oman.forward.pattern.create.singleton.Manager@5ca881b5
System.out.println(instance == instance2);
//true
ObjectOutputStream outputStream
= new ObjectOutputStream(new FileOutputStream("obj"));
outputStream.writeObject(instance);
outputStream.flush();
outputStream.close();
ObjectInputStream objectInputStream
= new ObjectInputStream(new FileInputStream("obj"));
Manager m = (Manager) objectInputStream.readObject();
objectInputStream.close();
System.out.println(m);
//com.oman.forward.pattern.create.singleton.Manager@4edde6e5
System.out.println(instance == m);
//false
}
}
我们看到反序列化后的对象和原来对象不一致。我们再来看看枚举的序列化和反序列化:
public enum EnumSingle {
INSTANCE;
public void test() {
System.out.println("test");
}
public static void main(String[] args) throws Exception {
EnumSingle single = EnumSingle.INSTANCE;
EnumSingle single2 = EnumSingle.INSTANCE;
System.out.println(single);//INSTANCE
System.out.println(single2);//INSTANCE
System.out.println(single == single2);//true
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("single"));
outputStream.writeObject(single);
outputStream.flush();
outputStream.close();
ObjectInputStream objectInputStream
= new ObjectInputStream(new FileInputStream("single"));
EnumSingle m = (EnumSingle) objectInputStream.readObject();
objectInputStream.close();
System.out.println(m);//INSTANCE
System.out.println(single == m);//true
}
}
从运行结果来看,序列化前和序列化后是同一个对象。
那么我们需要思考一下,为什么枚举能够保证线程安全?
其实枚举反编译后是继承自 Enum 类的,当一个 Java 类第一次被真正使用到的时候静态资源被初始化、Java 类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用 ClassLoader 的 loadClass 方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个 enum 类型是线程安全的。
另外枚举如何保证序列化后还是同一个对象的?
因为在序列化的时候 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf 方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 等方法。
普通的 Java 类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。
Kotlin 中的单例(这里只简单介绍两种)
饿汉式单例: kotlin 中的饿汉式单例只需要一个 object 关键字就可以实现,不信的话我们反编译看看字节码:
object Single
反编译后的 class 文件如下, 说明确实是饿汉式的单例。
public final class Single {
public static final Single INSTANCE;
private Single() {
}
static {
Single var0 = new Single();
INSTANCE = var0;
}
}
懒加载单例, 其实这里使用的是 kotlin 的延迟属性 lazy, 关于这个属性的具体内容大家可以自行学习。
class Single private constructor() {
companion object {
val instance: Single by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
Single()
}
}
}