美文网首页Java 杂谈Java设计模式设计
为什么说枚举是最好的Java单例实现方法?

为什么说枚举是最好的Java单例实现方法?

作者: LittleMagic | 来源:发表于2019-05-29 23:38 被阅读5次

很久没有写过接地气的东西了,今天随便写一个非常基础的。其实这篇文章也可以叫做《Java单例的破坏与防御方法》,无所谓了。

讲解Java单例实现方式及其原理的文章数不胜数,本文就不再多废话。在实际生产环境中,以下3种方式最常用,先复习一下。看官也可以试试能不能不参考任何资料,将下面的问题都回答正确。

Java单例的三种经典实现

双重检查锁(DCL)
public class DoubleCheckLockSingleton {
    private static volatile DoubleCheckLockSingleton instance;

    private DoubleCheckLockSingleton() {}

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

    public void tellEveryone() {
        System.out.println("This is a DoubleCheckLockSingleton " + this.hashCode());
    }
}
  • volatile关键字在此处起了什么作用?
  • 为何要执行两次instance == null判断?
静态内部类
public class StaticInnerHolderSingleton {
    private static class SingletonHolder {
        private static final StaticInnerHolderSingleton INSTANCE = new StaticInnerHolderSingleton();
    }

    private StaticInnerHolderSingleton() {}

    public static StaticInnerHolderSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public void tellEveryone() {
        System.out.println("This is a StaticInnerHolderSingleton" + this.hashCode());
    }
}
  • 这种方式是通过什么机制保证线程安全性延迟加载的?(注意,这是Java单例的两大要点,必须保证)
枚举
public enum EnumSingleton {
    INSTANCE;

    public void tellEveryone() {
        System.out.println("This is an EnumSingleton " + this.hashCode());
    }
}
  • Java枚举的本质是?
  • 这种方式又是通过什么机制保证线程安全性与延迟加载的?

复习完了。
在Java圣经《Effective Java》中,Joshua Bloch大佬如是说:

A single-element enum type is often the best way to implement a singleton.

为什么说枚举是(一般情况下)最好的Java单例实现呢?他也做出了简单的说明:

It is more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks.

大意就是,枚举单例可以有效防御两种破坏单例(即使单例产生多个实例)的行为:反射攻击序列化攻击(虽然我之前讲过“简单易懂的现代魔法”Unsafe,但它过于邪门歪道了,不算数)。言外之意就是前两种单例方式都会被破坏。那么我们就拿平时最常用的双重检查锁方式开刀来试试看。

如何破坏一个单例

反射攻击

直接上代码:

public class SingletonAttack {
    public static void main(String[] args) throws Exception {
        reflectionAttack();
    }

    public static void reflectionAttack() throws Exception {
        Constructor constructor = DoubleCheckLockSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton)constructor.newInstance();
        DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)constructor.newInstance();
        s1.tellEveryone();
        s2.tellEveryone();
        System.out.println(s1 == s2);
    }
}

执行结果如下:

This is a DoubleCheckLockSingleton 1368884364
This is a DoubleCheckLockSingleton 401625763
false

这种方法非常简单暴力,通过反射侵入单例类的私有构造方法并强制执行,使之产生多个不同的实例,这样单例就被破坏了。要防御反射攻击,只能在单例构造方法中检测instance是否为null,如果已不为null,就抛出异常。显然双重检查锁实现无法做这种检查,静态内部类实现则是可以的。

注意,不能在单例类中添加类初始化的标记位或计数值(比如boolean flagint count)来防御此类攻击,因为通过反射仍然可以随意修改它们的值。

序列化攻击

这种攻击方式只对实现了Serializable接口的单例有效,但偏偏有些单例就是必须序列化的。现在假设DoubleCheckLockSingleton类已经实现了该接口,上代码:

public class SingletonAttack {
    public static void main(String[] args) throws Exception {
        serializationAttack();
    }

    public static void serializationAttack() throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serFile"));
        DoubleCheckLockSingleton s1 = DoubleCheckLockSingleton.getInstance();
        outputStream.writeObject(s1);

        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("serFile")));
        DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)inputStream.readObject();
        s1.tellEveryone();
        s2.tellEveryone();
        System.out.println(s1 == s2);
    }
}

执行结果如下:

This is a DoubleCheckLockSingleton 777874839
This is a DoubleCheckLockSingleton 254413710
false

为什么会发生这种事?长话短说,在ObjectInputStream.readObject()方法执行时,其内部方法readOrdinaryObject()中有这样一句话:
obj = desc.isInstantiable() ? desc.newInstance() : null;

其中desc是类描述符。也就是说,如果一个实现了Serializable/Externalizable接口的类可以在运行时实例化,那么就调用newInstance()方法,使用其默认构造方法反射创建新的对象实例,自然也就破坏了单例性。要防御序列化攻击,就得将instance声明为transient,并且在单例中加入以下语句:

private Object readResolve() {
    return instance;
}

这是因为在上述readOrdinaryObject()方法中,会通过卫语句desc.hasReadResolveMethod()检查类中是否存在名为readResolve()的方法,如果有,就执行desc.invokeReadResolve(obj)调用该方法。readResolve()会用自定义的反序列化方法覆盖默认实现,因此强制它返回instance本身,就可以防止产生新的实例。

枚举单例的防御机制

对反射的防御

我们直接将上述reflectionAttack()方法中的类名改成EnumSingleton并执行,会发现报如下异常:

Exception in thread "main" java.lang.NoSuchMethodException: me.lmagics.singleton.EnumSingleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at me.lmagics.singleton.SingletonAttack.reflectionAttack(SingletonAttack.java:35)
    at me.lmagics.singleton.SingletonAttack.main(SingletonAttack.java:19)

这是因为所有Java枚举都隐式继承自Enum抽象类,而Enum抽象类根本没有无参构造方法,只有如下一个构造方法:

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

那么我们就改成获取这个有参构造方法,即:
Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
结果还是会抛出异常:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at me.lmagics.singleton.SingletonAttack.reflectionAttack(SingletonAttack.java:38)
    at me.lmagics.singleton.SingletonAttack.main(SingletonAttack.java:19)

来到Constructor.newInstance()方法中,有如下语句:

    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");

可见,JDK反射机制内部完全禁止了用反射创建枚举实例的可能性。

对序列化的防御

如果将serializationAttack()方法中的攻击目标换成EnumSingleton,那么我们就会发现s1和s2实际上是同一个实例,最终会打印出true。这是因为ObjectInputStream类中,对枚举类型有一个专门的readEnum()方法来处理,其简要流程如下:

  • 通过类描述符取得枚举单例的类型EnumSingleton;
  • 取得枚举单例中的枚举值的名字(这里是INSTANCE);
  • 调用Enum.valueOf()方法,根据枚举类型和枚举值的名字,获得最终的单例。

这种处理方法与readResolve()方法大同小异,都是以绕过反射直接获取单例为目标。不同的是,枚举对序列化的防御仍然是JDK内部实现的。

综上所述,枚举单例确实是目前最好的单例实现了。

相关文章

  • 为什么说枚举是最好的Java单例实现方法?

    很久没有写过接地气的东西了,今天随便写一个非常基础的。其实这篇文章也可以叫做《Java单例的破坏与防御方法》,无所...

  • 单例模式(Java内部类加载顺序)

    你真的会写单例模式吗——Java实现Android设计模式源码解析之单例模式深度分析 Java 的枚举类型:枚举的...

  • 单例模式中为什么用枚举更好

    枚举单例(Enum Singleton)是实现单例模式的一种新方式,尽管单例模式在java中已经存在很长时间了,但...

  • 通过枚举实现单例模式

    枚举单例(Enum Singleton)是实现单例模式的一种新方式,尽管单例模式在java中已经存在很长时间了,但...

  • 单例模式中为什么用枚举更好

    枚举单例(Enum Singleton)是实现单例模式的一种新方式,尽管单例模式在java中已经存在很长时间了,但...

  • 枚举单例

    描述 本文先反编译枚举,再使用枚举实现单例 枚举 单例 单例源码 单例反编译 引用 http://www.benf...

  • 单例模式之枚举类enum

    通过枚举实现单例模式 枚举类实现单例模式的优点 对于饿汉式单例模式和懒汉式单例模式了解的同学,使用以上两种单例模式...

  • java单例模式小结

    双检索实现的单例,是线程安全的。 枚举类型实现的单例,目前比较推荐

  • 【设计模式】单例模式的实现

    节选自:Java实现单例模式(懒汉式、饿汉式、双重检验锁、静态内部类方式、枚举方式)_常今-CSDN博客 方法一:...

  • 为什么java中用枚举实现单例模式会更好

    代码简洁 用枚举实现的单例: 这是我们通常写枚举单例的方式,它可能包含实例变量和实例方法,但是简单来说我什么都没用...

网友评论

    本文标题:为什么说枚举是最好的Java单例实现方法?

    本文链接:https://www.haomeiwen.com/subject/pnvytctx.html