单例类作为23种设计模式当中最常用的设计模式,实现方式有很多种,比较流行的是DCL(DoubleCheckLock)双重检查的实现,线程安全,又比较好,除了存在序列化的问题之外,还算不错,如果对DCL模式还不熟悉的可以看下我之前的博客,: 如何破坏双重校验锁的单例模式
最完美的实现方式其实是枚举,你用其他方式去实现单例,需要考虑很多问题,线程安全,序列化对单例模式的破坏。
关于What is an efficient way to implement a singleton pattern in Java?,stackOverflow有一条高赞的回答,如下图所示
EffectiveJava中明确表达过一个观点:
使用枚举实现单例的方法虽然还没有被广泛采用,但是单元素的枚举类型已经成为实现Signleton的最佳方法
其实在单例模式中,最不容易控制的问题是线程安全问题。
如果我们用代码实现单例,仅仅需要几行代码就可以解决
public enum Singleton {INSTANCE;public void
}
接下来我们再看双重锁校验的代码
public class Singleton implements Serializable {private static volatile Singleton singleton;private Singleton() {}public static Singleton getSingleton() {if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {singleton = new Singleton();}}}return singleton;}// 防止序列化private Object readResolve() {return singleton;}}
通过对比我们发现代码比较臃肿,这是因为大部分代码都是在线程安全和锁粒度之间做权衡,另外还要解决反序列化破坏单例模式的问题,不知不觉代码就写得复杂了,反观枚举类型,简洁明了
其实并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不再需要我们关心而已,也就是说,其实在底层还是做了线程安全方面的保证的。
定义枚举时使用的enum和class一样,也是Java中的一个关键字,就像class对应一个Class类一样,enum也对应一个Enum类
我们用javac 编译下文件,然后再用jad工具执行jad SingletonEnum.class
会生成Singleton.jad文件,我们可以直接用文本编辑器查看
public final class SingletonEnum extends Enum
{public static SingletonEnum[] values(){return (SingletonEnum[])$VALUES.clone();}public static SingletonEnum valueOf(String s){return (SingletonEnum)Enum.valueOf(other/SingletonEnum, s);}private SingletonEnum(String s, int i){super(s, i);}public void method(){}public static final SingletonEnum INSTANCE;private static final SingletonEnum $VALUES[];static {INSTANCE = new SingletonEnum("INSTANCE", 0);$VALUES = (new SingletonEnum[] {INSTANCE});}
}
可以看到代码中有一个static修饰的静态代码块,意味着在类加载阶段的加载阶段之后,会被调用进行初始化,那么我们知道,当一个Java类第一次被真正使用时静态资源被初始化,Java类的加载和初始化过程都是线程安全的,因为Java虚拟机在加载枚举类时,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全,如图所示
所以,创建一个enum类时线程安全的。也就是说我们定义的一个枚举在第一次被使用时,会被虚拟机加载并初始化,而这个过程是线程安全的。基于类加载的特性,这种实现方式天生就是安全的。
接着有人可能会说,枚举可以解决反序列化的问题吗?
答案是可以的
因为普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象,所以,即使单例中的构造函数是私有的,也会被反射破坏,由于反序列化后的对象是重新new出来的,所以这就破坏了单例模式。
但是枚举的反序列化并不是通过反射实现的,也就不会发生反序列化导致的破坏问题
在对枚举进行序列化是Java仅将枚举对象name属性输出到结果中,反序列化则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象的,同时,编译器是不允许任何对这种序列化机制的定制的,因此仅用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法的,
valueOf方法如下:
上述代码会尝试从调用enumType这个class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就抛出异常,我们接着来看这个方法调用了什么
核心代码在于getEnumConstantsShared(),这一步获取到了一个map对象并将其赋值给enumConstantsDirectory,而这个方法又以反射的方式调用了enumType这个类型的values()静态方法,也就是上面编译器帮我们创建的方法,
public static SingletonEnum[] values(){return (SingletonEnum[])$VALUES.clone();}
根据Java规范的规定,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,也就是说,每一个枚举项在JVM中都是单例