世上没有白费的努力,也没有碰巧的成功,全部的无意插柳,最后都会是水到渠成。面试
“你知道茴香豆的‘茴’字有几种写法吗?”
纠结单例模式有几种写法有用吗?有点用,面试中常常选择其中一种或几种写法做为话头,考查设计模式和代码风格的同时,还很容易扩展到其余问题。
这里讲解几种经常使用的写法,但切忌生搬硬套。大致可分为4类,下面分别介绍他们的基本形式、变种及特色。设计模式
饱汉是变种最多的单例模式。咱们从饱汉出发,经过其变种逐渐了解实现单例模式时须要关注的问题。安全
饱汉,即已经吃饱,不着急再吃,饿的时候再吃。因此他就先不初始化单例,等第一次使用的时候再初始化,即“懒加载”。微信
// 饱汉 // UnThreadSafe public class Singleton1 { private static Singleton1 singleton = null; private Singleton1() {} public static Singleton1 getInstance() { if(singleton == null) { singleton = new Singleton1(); } return singleton; } }
饱汉模式的核心就是懒加载。好处是更启动速度快、节省资源,一直到实例被第一次访问,才须要初始化单例;小坏处是写起来麻烦,大坏处是线程不安全,if语句存在竞态条件。多线程
写起来麻烦不是大问题,可读性好啊。所以,单线程环境下,基础饱汉是笔者最喜欢的写法。但多线程环境下,基础饱汉就完全不可用了。下面的几种变种都在试图解决基础饱汉线程不安全的问题。并发
最粗暴的犯法是用synchronized关键字修饰getInstance()方法,这样能达到绝对的线程安全。性能
// 饱汉 // ThreadSafe public class Singleton1_1 { private static Singleton1_1 singleton = null; private Singleton1_1() {} public synchronized static Singleton1_1 getInstance() { if(singleton == null) { singleton = new Singleton1_1(); } return singleton; } }
变种1的好处是写起来简单,且绝对线程安全;坏处是并发性能极差,事实上彻底退化到了串行。单例只须要初始化一次,但就算初始化之后,synchronized的锁也没法避开,从而getInstance()彻底变成了串行操做。性能不敏感的场景建议使用。flex
变种2是“臭名昭著”的DCL 1.0。this
针对变种1中单例初始化后锁仍然没法避开的问题,变种2在变种1的外层又套了一层check,加上synchronized内层的check,即所谓“双重检查锁”(Double Check Lock,简称DCL)。spa
// 饱汉 // UnThreadSafe public class Singleton1_2 { private static Singleton1_2 singleton = null; public int f1 = 1; // 触发部分初始化问题 public int f2 = 2; private Singleton1_2() {} public static Singleton1_2 getInstance() { // may get half object if(singleton == null) { synchronized(Singleton1_2.class) { if(singleton == null) { singleton = new Singleton1_2(); } } } return singleton; } }
变种2的核心是DCL,看起来变种2彷佛已经达到了理想的效果:懒加载+线程安全。惋惜的是,正如注释中所说,DCL仍然是线程不安全的,因为指令重排序,你可能会获得“半个对象”,即”部分初始化“问题。
变种3专门针对变种2,可谓DCL 2.0。
针对变种3的“半个对象”问题,变种3在instance上增长了volatile关键字,原理见上述参考。
// 饱汉 // ThreadSafe public class Singleton1_3 { private static volatile Singleton1_3 singleton = null; public int f1 = 1; // 触发部分初始化问题 public int f2 = 2; private Singleton1_3() {} public static Singleton1_3 getInstance() { if(singleton == null) { synchronized(Singleton1_3.class) { // must be a complete instance if(singleton == null) { singleton = new Singleton1_3(); } } } return singleton; } }
多线程环境下,变种3更适用于性能敏感的场景。但后面咱们将了解到,就算是线程安全的,还有一些办法能破坏单例。
固然,还有不少方式,能经过与volatile相似的方式防止部分初始化。读者可自行阅读内存屏障相关内容,但面试时不建议主动装逼。
与饱汉相对,饿汉很饿,只想着尽早吃到。因此他就在最先的时机,即类加载时初始化单例,之后访问时直接返回便可。
// 饿汉 // ThreadSafe public class Singleton2 { private static final Singleton2 singleton = new Singleton2(); private Singleton2() {} public static Singleton2 getInstance() { return singleton; } }
饿汉的好处是天生的线程安全(得益于类加载机制),写起来超级简单,使用时没有延迟;坏处是有可能形成资源浪费(若是类加载后就一直不使用单例的话)。
值得注意的时,单线程环境下,饿汉与饱汉在性能上没什么差异;但多线程环境下,因为饱汉须要加锁,饿汉的性能反而更优。
咱们既但愿利用饿汉模式中静态变量的方便和线程安全;又但愿经过懒加载规避资源浪费。Holder模式知足了这两点要求:核心仍然是静态变量,足够方便和线程安全;经过静态的Holder类持有真正实例,间接实现了懒加载。
// Holder模式 // ThreadSafe public class Singleton3 { private static class SingletonHolder { private static final Singleton3 singleton = new Singleton3(); private SingletonHolder() {} } private Singleton3() {} /** * 勘误:多写了个synchronized。。 public synchronized static Singleton3 getInstance() { return SingletonHolder.singleton; } */ public static Singleton3 getInstance() { return SingletonHolder.singleton; } }
相对于饿汉模式,Holder模式仅增长了一个静态内部类的成本,与饱汉的变种3效果至关(略优),都是比较受欢迎的实现方式。一样建议考虑。
用枚举实现单例模式,至关好用,但可读性是不存在的。
将枚举的静态成员变量做为单例的实例:
// 枚举 // ThreadSafe public enum Singleton4 { SINGLETON; }
代码量比饿汉模式更少。但用户只能直接访问实例Singleton4.SINGLETON——事实上,这样的访问方式做为单例使用也是恰当的,只是牺牲了静态工厂方法的优势,如没法实现懒加载。
Java的枚举是一个“丑陋但好用的语法糖”。
经过反编译打开语法糖,就看到了枚举类型的本质,简化以下:
// 枚举 // ThreadSafe public class Singleton4 extends Enum < Singleton4 > {... public static final Singleton4 SINGLETON = new Singleton4();... }
本质上和饿汉模式相同,区别仅在于公有的静态成员变量。
这一部分与单例没什么关系,能够跳过。若是选择阅读也请认清这样的事实:虽然枚举至关灵活,但如何恰当的使用枚举有必定难度。一个足够简单的典型例子是TimeUnit类,建议有时间耐心阅读。
上面已经看到,枚举型单例的本质仍然是一个普通的类。实际上,咱们能够在枚举型型单例上增长任何普通类能够完成的功能。要点在于枚举实例的初始化,能够理解为实例化了一个匿名内部类。为了更明显,咱们在Singleton4_1中定义一个普通的私有成员变量,一个普通的公有成员方法,和一个公有的抽象成员方法,以下:
// 枚举 // ThreadSafe public enum Singleton4_1 { SINGLETON("enum is the easiest singleton pattern, but not the most readable") { public void testAbsMethod() { print(); System.out.println("enum is ugly, but so flexible to make lots of trick"); } }; private String comment = null; Singleton4_1(String comment) { this.comment = comment; } public void print() { System.out.println("comment=" + comment); } abstract public void testAbsMethod(); public static Singleton4_1 getInstance() { return SINGLETON; } }
这样,枚举类Singleton4_1中的每个枚举实例不只继承了父类Singleton4_1的成员方法print(),还必须实现父类Singleton4_1的抽象成员方法testAbsMethod()。
上面的分析都忽略了反射和序列化的问题。经过反射或序列化,咱们仍然可以访问到私有构造器,建立新的实例破坏单例模式。此时,只有枚举模式能自然防范这一问题。反射和序列化笔者还不太了解,但基本原理并不难,能够在其余模式上手动实现。
下面继续忽略反射和序列化的问题,作个总结回味一下: