单例模式推荐写法--枚举实现单例

小小的单例模式看着简单,其实里面道道着实不少。不仅要在多线程下保证实例唯一,也要能抵御序列化以及反射对单例的破坏。
要同时解决这么多问题,说难也难,说简单也简单。在《Effective Java》这本书中最推荐的单例写法就是使用枚举来实现单例。枚举类天然的能同时满足多线程安全问题以及抵御序列化和反射对单例的破坏的问题。

枚举单例实现

/**
 * @Author: ming.wang
 * @Date: 2019/2/22 14:05
 * @Description: 枚举实现单例
 */
public enum EnumInstance {
    INSTANCE,;
    private Date birthDay;

    public Date getBirthDay() {
        return birthDay;
    }

    public void setBirthDay(Date birthDay) {
        this.birthDay = birthDay;
    }

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }
}

分析

下面我们分析一下,为什么枚举类能起到如此“逆天”的功能。

  • 枚举vs多线程安全
    首先我们分析一下,为什么枚举类可以保证线程安全。此处我们需要用到一个很牛叉的反编译工具jad,可支持linux、windows和苹果系统。下载完之后,我们使用jad来反编译一下EnumInstance.class(命令为jad \..\EnumInstance.class),然后生成了EnumInstance.jad文件,我们打开它
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumInstance.java

package com.wangming.pattern.creational.singleton;

import java.util.Date;

public final class EnumInstance extends Enum
{

    public static EnumInstance[] values()
    {
        return (EnumInstance[])$VALUES.clone();
    }

    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/wangming/pattern/creational/singleton/EnumInstance, name);
    }

    private EnumInstance(String s, int i)
    {
        super(s, i);
    }

    public Date getBirthDay()
    {
        return birthDay;
    }

    public void setBirthDay(Date birthDay)
    {
        this.birthDay = birthDay;
    }

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }

    public static final EnumInstance INSTANCE;
    private Date birthDay;
    private static final EnumInstance $VALUES[];

    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

通过反编译之后,一切秘密尽在眼前,原来它内部是执行了静态代码块,和饿汉式代码有异曲同工之妙,前面我们分析了当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

  • 枚举vs序列化和反序列化
    我们贴上完整的代码测试
package com.wangming.pattern.creational.singleton;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Date;

/**
 * @Author: ming.wang
 * @Date: 2019/2/21 16:05
 * @Description: 使用反射或反序列化来破坏单例
 */
public class DestroySingletonTest {


    public static void main(String[] args) throws Exception {
        //序列化方式破坏单例   测试
        serializeDestroyMethod();

        //反射方式破坏单例模式 测试
//        reflectMethod();

    }

    private static void reflectMethod() throws  Exception {

//        reflectHungryMethod();
//        reflectLazyMethod();
        reflectLazyMethod2();

    }

    private static void reflectHungryMethod() throws Exception {
        //同理StaticInnerClassSingleton

        HungrySingleton hungrySingleton = null;
        HungrySingleton hungrySingleton_new = null;

        Class singletonClass = HungrySingleton.class;
        Constructor declaredConstructor = singletonClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);

        hungrySingleton = HungrySingleton.getInstance();
        hungrySingleton_new = (HungrySingleton) declaredConstructor.newInstance();

        System.out.println(hungrySingleton == hungrySingleton_new);
    }

    /**
     * 验证使用对象空判断是否可抵御反射攻击
     * @throws Exception
     */
    private static void reflectLazyMethod() throws Exception {
        LazySingleton lazySingleton = null;
        LazySingleton lazySingleton_new = null;

        Class singletonClass = LazySingleton.class;
        Constructor declaredConstructor = singletonClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);

        lazySingleton = LazySingleton.getInstance();
        lazySingleton_new = (LazySingleton) declaredConstructor.newInstance();

        System.out.println(lazySingleton == lazySingleton_new);
    }

    /**
     * 验证使用标志位是否可抵御反射攻击
     * @throws Exception
     */
    private static void reflectLazyMethod2() throws Exception {
        LazySingleton lazySingleton = null;
        LazySingleton lazySingleton_new = null;

        Class singletonClass = LazySingleton.class;
        Constructor declaredConstructor = singletonClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);

        lazySingleton_new = (LazySingleton) declaredConstructor.newInstance();
        Field flag = singletonClass.getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(lazySingleton_new,true);
        lazySingleton = LazySingleton.getInstance();

        System.out.println(lazySingleton == lazySingleton_new);
    }


    private static void serializeDestroyMethod() throws IOException, ClassNotFoundException {
//        HungrySingleton intance=null;
//        HungrySingleton intance_new=null;

//        StaticInnerClassSingleton intance = null;
//        StaticInnerClassSingleton intance_new = null;

        EnumInstance intance = null;
        EnumInstance intance_new = null;

//        hungrySingleton=HungrySingleton.getInstance();
//        intance = StaticInnerClassSingleton.getInstance();
        intance=EnumInstance.getInstance();
        intance.setBirthDay(new Date());

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(intance);

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
//        hungrySingleton_new= (HungrySingleton) ois.readObject();
//        intance_new = (StaticInnerClassSingleton) ois.readObject();
        intance_new = (EnumInstance) ois.readObject();

        System.out.println(intance == intance_new);
        System.out.println(intance.getBirthDay() == intance_new.getBirthDay());
    }
}

运行结果是两个true。原因我们也简单提示一下,在讨论单例模式的攻击之序列化与反序列化这篇文章中,我们分析了ObjectInputStream.readObject()方法,其中一处代码

....
                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));
....

很显然我们此时是要走case TC_ENUM: return checkResolve(readEnum(unshared));这个分支。然后看readEnum(unshared)方法,你就知道为啥了。

  • 枚举vs反射
    为啥枚举能抵御几乎万能的反射呢?原因就在Constructor.newInstance()方法(Class.newInstance()最终调用的也是这个方法)。我们跟进去看下源码
....
@CallerSensitive
    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;
    }

.....

看到了吗if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");当判断是枚举类的时候,就直接抛出异常了。

结论

上面的疑惑基本解开,我们在运用单例模式的时候,最推荐的做法就是使用枚举类来实现单例!

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

推荐阅读更多精彩内容