饱汉模式
饱汉是变种最多的单例模式。我们从饱汉出发,通过其变种逐渐了解实现单例模式时需要关注的问题。
基础的饱汉
饱汉,即已经吃饱,不着急再吃,饿的时候再吃。所以他就先不初始化单例,等第一次使用的时候再初始化,即“懒加载”。
// 饱汉
// UnThreadSafe
public class Singleton1 {
private static Singleton1 singleton = null;
private Singleton1() {
}
public static Singleton1 getInstance() {
if (singleton == null) {
singleton = new Singleton1();
}
return singleton;
}
}
饱汉模式的核心就是懒加载。好处是更启动速度快、节省资源,一直到实例被第一次访问,才需要初始化单例;小坏处是写起来麻烦,大坏处是线程不安全,if语句存在竞态条件。
写起来麻烦不是大问题,可读性好啊。因此,单线程环境下,基础饱汉是笔者最喜欢的写法。但多线程环境下,基础饱汉就彻底不可用了。下面的几种变种都在试图解决基础饱汉线程不安全的问题。
饱汉 - 变种 1
最粗暴的犯法是用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()完全变成了串行操作。性能不敏感的场景建议使用。
饱汉 - 变种 2
变种2是“臭名昭著”的DCL 1.0。
针对变种1中单例初始化后锁仍然无法避开的问题,变种2在变种1的外层又套了一层check,加上synchronized内层的check,即所谓“双重检查锁”(Double Check Lock,简称DCL)。
// 饱汉
// 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仍然是线程不安全的,由于指令重排序,你可能会得到“半个对象”,即”部分初始化“问题。
volatile如何保持内存可见性: volatile关键字修饰的变量看到的随时是自己的最新值。
volatile如何防止指令重排: volatile关键字通过“内存屏障”
来防止指令被重排序。
问题:不加volatile,可能因为指令重拍导致 获取是是半个对象:
eg:
1 | instance = new Singleton(); |
它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令:
1 2 3 | memory = allocate(); //1:分配对象的内存空间 initInstance(memory); //2:初始化对象(对f1、f2初始化) instance = memory; //3:设置instance指向刚分配的内存地址 |
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序
,经过重排序后如下:
1 2 3 | memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置instance指向刚分配的内存地址(此时对象还未初始化) ctorInstance(memory); //2:初始化对象 |
可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,这段崭新的内存还没有初始化——即,引用instance指向了一个”被部分初始化的对象”。此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。
解决这个该问题,只需要将instance声明为volatile变量:
1 | private static volatile Singleton instance; |
饱汉 - 变种 3
变种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;
}
}