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

很久没有写过接地气的东西了,今天随便写一个非常基础的。其实这篇文章也可以叫做《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内部实现的。

综上所述,枚举单例确实是目前最好的单例实现了,不仅写法非常简单,并且JDK能够保证其安全性,不需要我们做额外的工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341