为何我墙裂建议你们使用枚举来实现单例。

关于单例模式,个人博客中有不少文章介绍过。做为23种设计模式中最为经常使用的设计模式,单例模式并无想象的那么简单。由于在设计单例的时候要考虑不少问题,好比线程安全问题、序列化对单例的破坏等。html

单例相关文章一览:java

设计模式(二)——单例模式设计模式

设计模式(三)——JDK中的那些单例安全

单例模式的七种写法并发

单例与序列化的那些事儿oracle

不使用synchronized和lock,如何实现一个线程安全的单例?函数

不使用synchronized和lock,如何实现一个线程安全的单例?(二)spa

若是你对单例不是很了解,或者对于单例的线程安全问题以及序列化会破坏单例等问题不是很清楚,能够先阅读以上文章。上面六篇文章看完以后,相信你必定会对单例模式有更多,更深刻的理解。线程

咱们知道,单例模式,通常有七种写法,那么这七种写法中,最好的是哪种呢?为何呢?本文就来抽丝剥茧一下。设计

哪一种写单例的方式最好

在StakcOverflow中,有一个关于What is an efficient way to implement a singleton pattern in Java?的讨论:

单例

如上图,得票率最高的回答是:使用枚举。

回答者引用了Joshua Bloch大神在《Effective Java》中明确表达过的观点:

使用枚举实现单例的方法虽然尚未普遍采用,可是单元素的枚举类型已经成为实现Singleton的最佳方法。

若是你真的深刻理解了单例的用法以及一些可能存在的坑的话,那么你也许也能获得相同的结论,那就是:使用枚举实现单例是一种很好的方法。

枚举单例写法简单

若是你看过《单例模式的七种写法》中的实现单例的全部方式的代码,那就会发现,各类方式实现单例的代码都比较复杂。主要缘由是在考虑线程安全问题。

咱们简单对比下“双重校验锁”方式和枚举方式实现单例的代码。

“双重校验锁”实现单例:

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;  
    }  
}  
复制代码

枚举实现单例:

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  
复制代码

相比之下,你就会发现,枚举实现单例的代码会精简不少。

上面的双重锁校验的代码之因此很臃肿,是由于大部分代码都是在保证线程安全。为了在保证线程安全和锁粒度之间作权衡,代码不免会写的复杂些。可是,这段代码仍是有问题的,由于他没法解决反序列化会破坏单例的问题。

枚举可解决线程安全问题

上面提到过。使用非枚举的方式实现单例,都要本身来保证线程安全,因此,这就致使其余方法必然是比较臃肿的。那么,为何使用枚举就不须要解决线程安全问题呢?

其实,并非使用枚举就不须要保证线程安全,只不过线程安全的保证不须要咱们关心而已。也就是说,其实在“底层”仍是作了线程安全方面的保证的。

那么,“底层”到底指的是什么?

这就要说到关于枚举的实现了。这部份内容能够参考个人另一篇博文深度分析Java的枚举类型—-枚举的线程安全性及序列化问题,这里我简单说明一下:

定义枚举时使用enum和class同样,是Java中的一个关键字。就像class对应用一个Class类同样,enum也对应有一个Enum类。

经过将定义好的枚举反编译,咱们就能发现,其实枚举在通过javac的编译以后,会被转换成形如public final class T extends Enum的定义。

并且,枚举中的各个枚举项同事经过static来定义的。如:

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}
复制代码

反编译后代码为:

public final class T extends Enum
{
    //省略部份内容
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}
复制代码

了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载以后被初始化,咱们在深度分析Java的ClassLoader机制(源码级别)Java类的加载、连接和初始化两个文章中分别介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(由于虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。因此,建立一个enum类型是线程安全的。

也就是说,咱们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而咱们知道,解决单例的并发问题,主要解决的就是初始化过程当中的线程安全问题。

因此,因为枚举的以上特性,枚举实现的单例是天生线程安全的。

枚举可解决反序列化会破坏单例的问题

前面咱们提到过,就是使用双重校验锁实现的单例实际上是存在必定问题的,就是这种单例有可能被序列化锁破坏,关于这种破坏及解决办法,参看单例与序列化的那些事儿,这里不作更加详细的说明了。

那么,对于序列化这件事情,为何枚举又有先天的优点了呢?答案能够在Java Object Serialization Specification 中找到答案。其中专门对枚举的序列化作了以下规定:

serialization

大概意思就是:在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是经过java.lang.EnumvalueOf方法来根据名字查找枚举对象。同时,编译器是不容许任何对这种序列化机制的定制的,所以禁用了writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve等方法。

普通的Java类的反序列化过程当中,会经过反射调用类的默认构造函数来初始化对象。因此,即便单例中构造函数是私有的,也会被反射给破坏掉。因为反序列化后的对象是从新new出来的,因此这就破坏了单例。

可是,枚举的反序列化并非经过反射实现的。因此,也就不会发生因为反序列化致使的单例破坏问题。这部份内容在深度分析Java的枚举类型—-枚举的线程安全性及序列化问题中也有更加详细的介绍,还展现了部分代码,感兴趣的朋友能够前往阅读。

总结

在全部的单例实现方式中,枚举是一种在代码写法上最简单的方式,之因此代码十分简洁,是由于Java给咱们提供了enum关键字,咱们即可以很方便的声明一个枚举类型,而不须要关心其初始化过程当中的线程安全问题,由于枚举类在被虚拟机加载的时候会保证线程安全的被初始化。

除此以外,在序列化方面,Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就能够避免反序列化过程当中因为反射而致使的单例被破坏问题。

相关文章
相关标签/搜索