一、什么是DCL
DCL的全拼是double check lock,也就是双重锁判断机制。
二、回顾几种单例
1、饿汉式
public class SingleInstance {
private SingleInstance(){}
private static final SingleInstance INSTANCE=new SingleInstance();
public static SingleInstance getInstance(){
return INSTANCE;
}
}
优点:线程安全、调用效率高
缺点:不能延时加载
2、懒汉式
public class SingleInstance {
private SingleInstance(){}
private static SingleInstance INSTANCE;
public static synchronized SingleInstance getInstance(){
if (INSTANCE==null){
INSTANCE=new SingleInstance();
}
return INSTANCE;
}
}
优点:线程安全、能延时加载。
缺点:锁的粒度太大,导致调用效率不高。
3、DCL
public class SingleInstance {
private SingleInstance() {}
private static volatile SingleInstance INSTANCE;
public static SingleInstance getInstance() {
if (INSTANCE == null) {
synchronized (SingleInstance.class) {
if (INSTANCE == null) {
INSTANCE = new SingleInstance();
}
}
}
return INSTANCE;
}
}
优点:线程安全、调用效率高、能延迟加载。
4、匿名内部类
public class SingleInstance {
private SingleInstance() {}
private static final class HolderClass{
private static final SingleInstance INSTANCE=new SingleInstance();
}
public static SingleInstance getInstance() {
return HolderClass.INSTANCE;
}
}
优点:线程安全,调用效率高,可以延时加载。
三、DDL单例为什么要加volatile
我们把上面写的DDL单例拿过来,加入不加volatile,如下:
public class SingleInstance {
private SingleInstance() {}
private static SingleInstance INSTANCE;
public static SingleInstance getInstance() {
if (INSTANCE == null) {
synchronized (SingleInstance.class) {
if (INSTANCE == null) {
INSTANCE = new SingleInstance();
}
}
}
return INSTANCE;
}
}
当 INSTANCE = new SingleInstance() 创建实例对象时,并不是原子操作,它是分三步来完成的:
- 创建内存空间。
- 执行构造函数,初始化(init)
- 将INSTANCE引用指向分配的内存空间
上述正常步骤按照1–>2–>3来执行的,但是,我们知道,JVM为了优化指令,提高程序运行效率,允许指令重排序。正是有了指令重排序的存在,那么就有可能按照1–>3–>2步骤来执行,这时候,当线程a执行步骤3完毕,在执行步骤2之前,被切换到线程b上,这时候instance判断为非空,此时线程b直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。
synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
volatile关键字其中一个作用就是禁止指令重排序,所以DCL单例必须要加volatile
volatile作用:
- 保证被修饰的变量对所有线程的可见性。
- 禁止指令重排序优化。