单例模式中为什么用枚举更好
引言
本文翻译自 Javarevisited。如需转载本文,请先参见文章末尾处的转载要求。
枚举单例(Enum Singleton)是实现单例模式的一种现代方式。尽管单例模式在 Java 中已存在很长时间,但利用枚举实现单例相对较新(枚举特性自 Java 5 引入)。本文将探讨为什么推荐使用枚举来实现单例模式,以及它与传统实现方式相比具有哪些优势。
1. 枚举写法简洁
写法简单是枚举单例最大的优点。如果你先前写过单例模式,应该知道即使使用双重检查锁定(Double-Checked Locking, DCL),也可能会因实现细节不当而创建不止一个实例。尽管 Java 5 在内存模型上做了大量改善(提供了 volatile 关键字来修饰变量),修复了部分问题,但对新手来说仍然比较棘手。
相比之下,枚举单例的实现非常简洁。以下分别展示了传统的 DCL 实现、静态工厂实现与枚举单例的对比。
枚举实现
下面这段代码是声明枚举实例的通常做法。它可能还包含实例变量和实例方法,但为了简单起见,此处未展示。需要注意的是,如果你正在使用实例方法,需要确保线程安全(如果它影响到其他对象的状态的话)。默认情况下,枚举实例的创建是线程安全的,但枚举中的其他任何方法由程序员自己负责。
/**
* Singleton pattern example using Java Enum
*/
public enum EasySingleton {
INSTANCE;
}你可以通过 EasySingleton.INSTANCE 来访问实例,这比调用 getInstance() 方法简单多了。
双重检查锁定(DCL)实现
下面代码展示了使用 DCL 方法实现的单例。getInstance() 方法要检查两次,确保实例 INSTANCE 是否为 null 或者已经实例化了,这也是为什么叫“双重检查锁定”模式。
/**
* Singleton pattern example with Double checked Locking
*/
public class DoubleCheckedLockingSingleton {
private volatile DoubleCheckedLockingSingleton INSTANCE;
private DoubleCheckedLockingSingleton() {}
public DoubleCheckedLockingSingleton getInstance() {
if (INSTANCE == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
// double checking Singleton instance
if (INSTANCE == null) {
INSTANCE = new DoubleCheckedLockingSingleton();
}
}
}
return INSTANCE;
}
}你可以使用 DoubleCheckedLockingSingleton.getInstance() 来获取实例。从创建一个懒加载(lazy loaded)线程安全单例来看,它的代码行数与枚举相比明显更多。枚举创建的单例在 JVM 层面上也能保证实例是线程安全的,且代码可以全部在一行内完成。
静态工厂实现
这是另一种常见的方式来实现单例模式。因为单例是静态的 final 变量,当类第一次加载到内存中的时候就初始化了,所以创建的实例固然是线程安全的。
/**
* Singleton pattern example with static factory method
*/
public class Singleton {
// initialized during class loading
private static final Singleton INSTANCE = new Singleton();
// to prevent creating another instance of Singleton
private Singleton() {}
public static Singleton getSingleton() {
return INSTANCE;
}
}你可以调用 Singleton.getSingleton() 获取实例。就我个人而言,很多时候更倾向于通过类加载静态字段的方式初始化,但请记住这不是懒加载形式的单例。
人们可能会争论有更好的方式去写单例用来替换 DCL 方法,但是每种方法有它自己的优点和缺点。
2. 枚举自动处理序列化
传统单例存在的另外一个问题是:一旦你实现了序列化接口,那么它们不再保持单例了。因为 readObject() 方法一直返回一个新的对象,就像 Java 的构造方法一样。你可以通过使用 readResolve() 方法来避免此事发生。
这样甚至还可以更复杂,如果你的单例类维持了其他对象的状态的话,因此你需要使它们成为 transient 的对象。但是枚举单例,JVM 对序列化有保证,天然防止了通过序列化破坏单例的情况。
3. 枚举实例创建是线程安全
正如在第一条中所说的,因为创建枚举默认就是线程安全的,你不需要担心双重检查锁定(DCL)的复杂性。JVM 在加载枚举类时保证了实例化的原子性。
总结
枚举单例有序列化和线程安全的保证,而且只要几行代码就能实现,是单例最好的实现方式之一。不过你仍然可以使用其它的方式来实现单例,但我仍然得不到一个更有信服力的原因不去使用枚举。如果你有的话,不妨告诉我。
说明:本文所述技术特性基于 Java 5 及以上版本。枚举单例模式在现代 Java 开发中依然适用且推荐。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/dan-li-mo-shi-zhong-wei-shen-me-yong-mei-ju-geng-hao.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。