引言

本文翻译自 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 开发中依然适用且推荐。