说到单例模式,大家应该都不陌生,毕竟它是应用最广泛的模式之一。
单例模式的主要实现形式
饿汉模式
饿汉模式是在声明静态对象时就已经初始化单例了。代码如下:
public class Singleton {
private static Singleton mInstance = new Singleton();
public static Singleton getInstance() {
return mInstance;
}
private Singleton() {}
}
缺点:无论使用还是不使用都会初始化,造成不必要的开销。
懒汉模式
懒汉模式实在第一次调用getInstance的时候去初始化。代码如下:
public class Singleton {
private static Singleton mInstance;
public static synchronized Singleton getInstance() {
if (mInstance == null) {
mInstance = new Singleton();
}
return mInstance;
}
private Singleton() {
}
}
优点:只有在第一次使用的时候才会去实例化单例。
缺点:每次调运都会去同步,造成不必要的同步开销。
double check lock(DCL)实现单例
DCL实现单例的优点是既能在需要的时候才初始化单例,又能保证线程安全,而且单例初始化后,调用getInstance没有同步开销。代码如下:
public class Singleton {
private static Singleton mInstance;
public static Singleton getInstance() {
if (mInstance == null) {
synchronized (Singleton.class) {
if (mInstance == null) {
mInstance = new Singleton();
}
}
}
return mInstance;
}
private Singleton() {
}
}
但是,这种模式存在缺陷,就是在高并发的情况下会出问题。这是为什么了?下面来分析一下。
mInstance = new Singleton();它不是一个原子操作,这个行代码最终会被编译成汇编指令,它大致做了三件事:
- 给Singleton实例分配内存空间
- 调运Singleton()初始化
- 将mInstance指向分配好的内存空间(这时mInstance就不为null了)
但是jvm为了优化指令,提高运算效率就会进行指令重排,导致2、3不一定是顺序执行的。也就是说执行的顺序可能是1-2-3,也可能是1-3-2。这就尴尬了,当A线程执行了1-3,此时mInstance已经不为null了,但是它指向的内存是不可用的,此时B线程调运了getInstance(),返现mInstance不为null,所以就直接使用了,这时就会出错。这就是DCL失效的问题,而且这种难以追踪难以重现的问题会隐藏很久。
指令重排是什么?下面来介绍一下
指令重排
指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。
不同的指令间可能存在数据依赖。比如下面计算圆的面积的语句:
double r = 2.3d;//(1)
double pi =3.1415926; //(2)
double area = pi* r * r; //(3)
area的计算依赖于r与pi两个变量的赋值指令。而r与pi无依赖关系。
as-if-serial语义是指:不管如何重排序(编译器与处理器为了提高并行度),(单线程)程序的结果不能被改变。这是编译器、Runtime、处理器必须遵守的语义。
虽然,(1) – happens before -> (2),(2) – happens before -> (3),但是计算顺序(1)(2)(3)与(2)(1)(3) 对于r、pi、area变量的结果并无区别。编译器、Runtime在优化时可以根据情况重排序(1)与(2),而丝毫不影响程序的结果。
指令重排序包括编译器重排序和运行时重排序。
防止指令重排
JDK1.5之后sun公司注意到了这个问题,就增加了volatile。可以使用volatile变量禁止指令重排序。变量在以volatile修饰后,会阻止JVM对与其相关的代码进行重排,达到按照既定顺序执行代码的目的。代码如下:
public class Singleton {
private static volatile Singleton mInstance;
public static Singleton getInstance() {
if (mInstance == null) {
synchronized (Singleton.class) {
if (mInstance == null) {
mInstance = new Singleton();
}
}
}
return mInstance;
}
private Singleton() {
}
}
静态内部类单例模式
DCL虽然解决了资源消耗、多余同步、线程安全等问题,但是在某些情况下,它还会出现实效的情况。在《java并发编程》一书中指出DCL是一种丑陋的写法,不赞成使用。并建议使用如下写法:
public class Singleton {
public static Singleton getInstance() {
return SingletonHolder.mInstance;
}
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton mInstance = new Singleton();
}
}
但加载Singleton类的时候,并不会去初始化mInstance,只有第一次调用getInstance的时候在回去初始化mInstacnce。第一次调用getInstance的时候,会导致虚拟机去加载SingletonHolder类,这时才会初始化mInstance。