单例在Java中应用很广泛,最近看了一些文章,想就单例的几种写法做一点归纳。
Example 1
1 | public class SingletonExample { |
这种办法比较简单明了,单例对象被声明为类的静态变量,在类被创建的初期,单例就会同步被创建。这种办法虽然简单,但依然可以避免多线程并发情况下可能创建多个对象的问题,这是依靠类加载机制保证的,因为加载类的时候只有单线程。
但因为对象的创建是在类被加载时,类的构建方法如果存在着很耗时的操作,那代价也挺大的,所以如果使用这种办法,可能需要将创建对象与初始化对象两个操作分离,即保证完对象唯一后,再去做余下的初始化工作 。
Example 2
1 | public class SingletonExample { |
这种办法可以说是Example 1的一种改良,它保证并发唯一原理上跟例一是一样的,都是根据类加载静态类型时单线程来保证的;但这种办法改善了例一的缺点,假如我们只是需要调用SingletonExample中的一个静态方法而并非需要使用其对象方法,如果使用例一则此时就会直接创建一个对象,可能与我们的期望不符;则例二就可以避免这种问题,当调用instance()时,才会开始加载内部和初始化静态类SingletonExampleHolder,单例也是这时候才创建。这种适合不需要在类加载时就创建的时候使用。
Example 3
1 | public class SingletonExample { |
这种办法也是比较简单粗暴的,同样也能够避免多线程下的问题,这个是依赖于同步锁来保证的。
但问题同样也很明显,用锁去保证实际上将多个并发变成了串行,当存在多个线程去获取SingletonExample单例时,来到instance()这个方法时就必须等待,性能损耗比较严重。
Example 4
1 | public class SingletonExample { |
这种写法通过两次校验来保证并发下的可靠性,民间称为DCL(double-checked locking,双重校验锁),其先判断INSTANCE是否为空,一般来说,单例创建后更多情况下调用instance时就会被此判断拦截而直接返回单例,从而避免上锁;而在首次创建时则通过锁来保证只有一个线程能进入创建单例,表面上来看既保证了性能,也保证并发下的可靠。
然而这种写法是有争议的。原因在于一些底层问题。由于我们的代码都是由编译器生成出来的,编译器有可能会根据一些优化规则,在不影响最终结果的前提下自动调整最终生成代码的的先后顺序、缓存(如使用寄存器而不使用主存作缓存),以及更新主存的时机等(具体可以参考编译原理中的例子),所以实际上运行的过程是一致的,但在并非底层的世界中却并非都一样。
当存在多个线程时,假设线程A,B,当A进入instance(),并进入到同步块中,此时判断到INSTANCE为空,则会去new一个新的SingletonExample对象,此时SingletonExample对象已经在内存创建了,准备去调用构造方法,此时线程B来了,它看到INSTANCE已经不为空了,就直接返回了部分构建的SingletonExample对象,此时去直接使用INSTANCE对象显示是会有问题的;
另外,DCL还存在另一个问题,当同步代码块已经完成SingletonExample的创建后,准备将局部储存中的数据更新主存,如果此时INSTANCE对象中存在其他成员(对象)的引用,则线程B获取到INSTANCE后可能还可以访问到旧的成员变量。
一种解决办法是使用volatile来保证每次读到的都是最新的数据,但了民是因为上面的原因,刷新到主存也是有顺序关系的,如果INSTANCE对象存在其他成员的引用,则需要将所有相关的成员都声明为volatile。
可见,使用DCL的方式创建单例比较麻烦,虽然上面提到的问题在JDK1.5以后声称已经解决了,但实际上这种办法去创建单例代码比较长,效果也不一定就是最好,所以以后还是少用为妙。
JDK 1.5以后的解决办法需要在原来的基础上添加voliate关键字同样可以解决这类问题:
1 | private static **voliate** SingletonExample INSTANCE = null; |
另外,WIKI也提供了另一种通过final关键字的解决办法:
1 | public class FinalWrapper<T> { |
即通过一个wrapper类来保证helper对象的唯一性,性能上估计跟voliate差不多.
Example 5
1 | public enum Singleton { |
枚举。因为枚举在编译后实际上生成的一个final类型,继承Enum类的类,其成员全部都是静态的,以上也说过静态是在类加载时赋值创建的,而类加载过程中是线程安全的,所以使用枚举来创建单例也是线程安全的。
另外,枚举类型也解决了单例中的序列化问题。
可以说是这种办法是创建枚举比较完美的办法,但由于声明是enum类型,可能在实际应用过程中并不都是适用,可以斟酌。
[参考资料]
http://javarevisited.blogspot.com/2014/05/double-checked-locking-on-singleton-in-java.html
http://javarevisited.blogspot.com/2012/07/why-enum-singleton-are-better-in-java.html
http://javarevisited.blogspot.com/2012/12/how-to-create-thread-safe-singleton-in-java-example.html