专栏首页BAT的乌托邦Java单例模式的7种写法中,为何用Enum枚举实现被认为是最好的方式?【享学Java】

Java单例模式的7种写法中,为何用Enum枚举实现被认为是最好的方式?【享学Java】

前言

如果看到标题就能表示赞同的小伙伴,那估摸你也肯定看过Joshua Bloch大神说过的这么一句话:单元素的枚举类型已经成为实现Singleton的最佳方法。我把它翻译成人话就是:实现单例模式的最佳方法是使用枚举

单例模式

单例模式(Singleton Pattern):确保一个类有且只有一个实例,并提供一个全局访问点。

在开发中,很多时候有一些对象其实我们只需要一个,例如:线程池(threadpool)缓存(cache)默认设置注册表(registry)日志对象等等,这个时候把它设计为单例模式是最好的选择。

Java中单例模式是一种广泛使用的设计模式,单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间(比如spring管理的无状态bean);还能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。

单例模式7种写法

单例模式的写法非常多,但很多写法存在一些不足,下面以示例的方式加以指出:

1、懒汉(线程不安全):
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  //私有构造函数
  
    public static Singleton getInstance() {  
	    if (instance == null) {  
	        instance = new Singleton();  
	    }  
	    return instance;  
    }  
}  

这种写法lazy loading(懒加载)很明显,但是一看就知道,存在线程安全问题,所以这种写法是被禁止的

2、懒汉(线程安全):
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
	    if (instance == null) {  
	        instance = new Singleton();  
	    }  
	    return instance;  
    }  
}  

显然加了个synchronized来保证线程安全,but,效率太低了,毕竟99.99%的情况下是不需要同步的,有点用力过猛。极力不推荐使用

3、饿汉:
public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    	return instance;  
    }  
}  

这种基于classloder机制避免了多线程的同步问题,初始化的时候就给装载了。但是现在,没有懒加载的效果了。这是最简单的一种实现,据我了解绝大部分小伙伴都是这些写单例模式的~

4、饿汉(变种):
public class Singleton {  
    private static Singleton instance = null;  
    static {  
      instance = new Singleton();  
    }  
    private Singleton (){}  
    public static Singleton getInstance() {  
    	return instance;  
    }  
} 

和上面差不多,都是在本类初始化即实例化instance。

5、静态内部类:
public class Singleton {  
	// 静态内部类
    private static class SingletonHolder {  
    	private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
	
    public static final Singleton getInstance() {  
    	return SingletonHolder.INSTANCE;  
    }  
}  

请注意这种方式和上面是存在不一样的地方的 。

据我了解:面试中能答出这种方式以及下面方式,都属加分项

刚分析了方式3、4都没有lazy loading效果。而这种方式Singleton类被装载了,instance不会被立马初始化,因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,显然它达到了`lazy loading效果

6、双重校验锁(懒汉)
public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
	    if (singleton == null) {  
	        synchronized (Singleton.class) {  
	        	// 注意此处还得有次判空~
		        if (singleton == null) {  
		            singleton = new Singleton();  
		        }  
	        }  
	    }  
	    return singleton;  
    }  
}  

使用到了volatile机制。这个是第二种方式的升级版,俗称双重检查锁定。既保证了效率,又保证了安全。代码稍微复杂点,但显得比较高级~

7、枚举
public enum Singleton {  
    INSTANCE;  
}  

使用枚举方式实现,也是本文的主菜。

这种方式是Effective Java作者Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊。 所以这种写法,是十分推荐的且是最优的




为何枚举方式是最好的单例实现方式?

前几种方式实现单例都有如下3个特点:

  1. 构造方法私有化
  2. 实例化的变量引用私有化
  3. 获取实例的方法共有

这种实现方式的问题就在低一点:私有化构造器并不保险。因为它抵御不了反射攻击,比如如下示例代码:

以大家最为常用的饿汉式为例,看我怎么攻击它

public class Singleton implements Serializable {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    	return instance;  
    }  
}

public class Main {

    public static void main(String[] args) throws Exception {
        Singleton s = Singleton.getInstance();

        // 拿到所有的构造函数,包括非public的
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        // 使用空构造函数new一个实例。即使它是private的~~~
        Singleton sReflection = constructor.newInstance();

        System.out.println(s); //com.fsx.bean.Singleton@1f32e575
        System.out.println(sReflection); //com.fsx.bean.Singleton@279f2327
        System.out.println(s == sReflection); // false
    }

}

运行输出:

com.fsx.bean.Singleton@1f32e575
com.fsx.bean.Singleton@279f2327
false

通过反射,竟然给所谓的单例创建出了一个新的实例对象。所以这种方式也还是存在不安全因素的。怎么破???如何解决??? 其实Joshua Bloch说了:可以在构造函数在被第二次调用的时候抛出异常。具体示例代码,可以参考枚举实现的源码,哈哈。

再看看它的序列化、反序列时会不会有问题。如下:

注意:JDK的序列化、反序列化底层并不是反射~~~

public class Main {

    public static void main(String[] args) throws Exception {
        Singleton s = Singleton.getInstance();

        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);


        System.out.println(s);
        System.out.println(deserialize);
        System.out.println(s == deserialize);

    }

}

运行结果:

com.fsx.bean.Singleton@452b3a41
com.fsx.bean.Singleton@6193b845
false

可以看出,序列化前后两个对象并不相等。所以它序列化也是不安全的

下面看看枚举大法

使用枚举实现单例极其的简单:

public enum EnumSingleton {
    INSTANCE;    
}

首先看看是否防御反射攻击

public class Main {

    public static void main(String[] args) throws Exception {
        EnumSingleton s = EnumSingleton.INSTANCE;

        // 拿到所有的构造函数,包括非public的
        Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        // 使用空构造函数new一个实例。即使它是private的~~~
        EnumSingleton sReflection = constructor.newInstance();

        System.out.println(s); //com.fsx.bean.Singleton@1f32e575
        System.out.println(sReflection); //com.fsx.bean.Singleton@279f2327
        System.out.println(s == sReflection); // false
    }

}

结果运行就报错:

Exception in thread "main" java.lang.NoSuchMethodException: com.fsx.bean.EnumSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.fsx.maintest.Main.main(Main.java:19)

这个看起来是因为没有空的构造函数导致的,还并不能下定义说防御了反射攻击。那它有什么构造函数呢,可以看它的父类Enum类:

// @since   1.5  它是所有Enum类的父类,是个抽象类
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
	// 这是它的唯一构造函数,接收两个参数(若没有自己额外指定构造函数的话~)
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    ...
}

既然它有这个构造函数,那我们就先拿到这个构造函数再创建对象试试:

public class Main {

    public static void main(String[] args) throws Exception {
        EnumSingleton s = EnumSingleton.INSTANCE;

        // 拿到所有的构造函数,包括非public的
        Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);// 拿到有参的构造器
        constructor.setAccessible(true);
        // 使用空构造函数new一个实例。即使它是private的~~~
        System.out.println("拿到了构造器:" + constructor);
        EnumSingleton sReflection = constructor.newInstance("testInstance", 1);

        System.out.println(s); //com.fsx.bean.Singleton@1f32e575
        System.out.println(sReflection); //com.fsx.bean.Singleton@279f2327
        System.out.println(s == sReflection); // false
    }

}

运行打印:

拿到了构造器:private com.fsx.bean.EnumSingleton(java.lang.String,int)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.fsx.maintest.Main.main(Main.java:22)

第一句输出了,表示我们是成功拿到了构造器Constructor对象的,只是在执行newInstance时候报错了。并且也提示报错在Constructor的417行,看看Constructor的源码处:

public final class Constructor<T> extends Executable {
	...
    public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		...
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
		...
	}
	...
}

主要是这一句:(clazz.getModifiers() & Modifier.ENUM) != 0。说明:反射在通过newInstance创建对象时,会检查该类**是否ENUM修饰**,如果是则抛出异常,反射失败,因此枚举类型对反射是绝对安全的。

那么,枚举对序列化、反序列化是否安全?

public class Main {

    public static void main(String[] args) {
        EnumSingleton s = EnumSingleton.INSTANCE;

        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);
        System.out.println(s == deserialize); //true
    }

}

结果是:true。因此:枚举类型对序列化、反序列也是安全的。

综上,可以得出结论:枚举是实现单例模式的最佳实践。毕竟使用它全都是优点:

  1. 反射安全
  2. 序列化/反序列化安全
  3. 写法简单
  4. 没有一个更有信服力的原因不去使用枚举

附:局变量比单例模式差在哪里?
  1. 不可延迟实例化
  2. 不能保证全局只有一个实例(因为使用者都可以自己new对象)

总结

单例模式作为最为简单的一种设计模式,可以说是用到了everywhere,它不仅仅是我们撸码中肯定会用到的,更是面必问的一道题。我相信你理解了本文之后,以后不管使用和面试,都能轻松应对~

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
本文分享自作者个人站点/博客:https://blog.csdn.net/f641385712复制
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 掌握Java枚举这几个知识点,日常开发就够啦

    春节来临之际,祝大家新年快乐哈。整理了Java枚举的相关知识,算是比较基础的,希望大家一起学习进步。

    捡田螺的小男孩
  • 深入理解《单例模式》之源码分析

    执行这段代码会发现o1<>o2,这就破坏了单例。 为什么呢?罪魁祸首就是如下代码,它是反射的newInstance()的底层实现。

    本人秃顶程序员
  • Java中枚举的线程安全性及序列化问题

    要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由...

    一个程序员的成长
  • 关于Java中枚举Enum的深入剖析

    在编程语言中我们,都会接触到枚举类型,通常我们进行有穷的列举来实现一些限定。Java也不例外。Java中的枚举类型为Enum,本文将对枚举进行一些比较深入的剖析...

    Java团长
  • 关于Java中枚举Enum的深入剖析

    在编程语言中我们,都会接触到枚举类型,通常我们进行有穷的列举来实现一些限定。Java也不例外。Java中的枚举类型为Enum,本文将对枚举进行一些比较深入的剖析...

    技术小黑屋
  • 设计模式 | 单例模式及典型应用

    单例是最常见的设计模式之一,实现的方式非常多,同时需要注意的问题也非常多。要内容:

    小旋锋
  • 深入理解 Java 枚举

    执行 javac ColorEn.java 命令,生成 ColorEn.class 文件。

    静默虚空
  • 2018-06-13 如何优雅高效的创建单例——枚举Enum

    Albert陈凯
  • 三歪问我为啥用枚举,枚举有哪些用法?

    在学习过程中,我们也只是在定义常量的时候,会意识到枚举的存在,而定义常量其实可以在类中实现,这时就会感觉枚举有点鸡肋。但在实际项目开发的过程中,枚举因相当迷人的...

    敖丙
  • 深入分析Java的enum

    JavaEdge
  • 用好Java中的枚举,真的没有那么简单!

    在本文中,我们将看到什么是 Java 枚举,它们解决了哪些问题以及如何在实践中使用 Java 枚举实现一些设计模式。

    Guide哥
  • 夯实Java基础系列14:深入理解Java枚举类

    枚举(enum)类型是Java 5新增的特性,它是一种新的类型,允许用常量来表示特定的数据片断,而且全部都以类型安全的形式来表示。

    Java技术江湖
  • Java中的枚举,竟然有这么多不为人知的知识点

    Java枚举,也称作Java枚举类型,是一种字段由一组固定常量集合组成的类型。枚举的主要目的是加强编译时类型的安全性。enum关键字是Java中的保留关键字。

    程序新视界
  • 深入分析 Java 的枚举 enum

    enum全称为enumeration,中文意为枚举,枚举简单的说就是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。

    JavaEdge
  • 走进 JDK 之 Enum

    什么是枚举?说实话,在我这些年的开发生涯中,用过枚举的次数大概两只手都可以数的过来。当然你不能说枚举一无是处,只能说是我对 Java 理解的还不够深刻,在可以使...

    路遥TM
  • 我向面试官讲解了单例模式,他对我竖起了大拇指

    单例模式相信大家都有所听闻,甚至也写过不少了,在面试中也是考得最多的其中一个设计模式,面试官常常会要求写出两种类型的单例模式并且解释其原理,废话不多说,我们开始...

    cxuan
  • 「JAVA」以周的案例引出Java 枚举,以单例设计模式实践Java 枚举

    解决方案:定义一个类Weekday来表示周1到周日,在类中分别使用7个常量来表示周1到周日,代码如下:

    老夫编程说
  • 不了解这12个语法糖,别说你会Java!

    本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,...

    用户5224393
  • 深入理解Enum(枚举类)

    你写了一个小程序,不过好久不用了,突然有一天,你想使用一下它。程序要想正确运行,需要将今天星期几存到数据库里。这个时候,你开始犯难了。

    说故事的五公子

扫码关注云+社区

领取腾讯云代金券