深入理解DCL(双重检测锁)单例
- 如果不使用双重检测锁我们可以直接使用synchronized关键字实现
// 懒汉式
public class Singleton {
// 单例对象
private Singleton singleton = null;
// 私有构造方法
private Singleton() {
}
// 获取单例
public static synchronized Singleton getSingleton() {
if (null == singleton) {
singleton = new Singleton();
}
return singleton;
}
/*
这个方法等价于上面的方法
public static Singleton getSingleton() {
synchronized (Singleton.class) {
if (null == singleton) {
singleton = new Singleton();
}
}
return singleton;
}
*/
}
上面这种方式可以实现线程安全的懒汉式单例模式,但是有个致命的的缺点:比如现在有一万个线程,只有一个线程可以获取到synchnized锁,9999个线程全部阻塞挂起,效率及其的低下
- 所以我们就引入了双重检测锁的方式
public class DCLSingleton {
// 单例
private static DCLSingletonsingleton = null;
// 私有构造方法
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
if (null == singleton) {
synchronized (DCLSingleton.class) {
if (null == singleton) {
singleton= new DCLSingleton();
}
}
}
return singleton;
}
}
也就是在synchronized的外层再加一次实例是否为空的判断
原因:如果第一个线程创建了实例化,其余的线程就不需要再次获取锁去内层判断实例是否为空,
这样就减少了线程挂起的过程,减少了cpu切换的开销
其实这种方法也是有问题的,在较小的并发量下并不会出现问题,只有在超大并发量下才会出现问题
在alibaba开发规范中明确指出,在使用双重检查锁时要使用volatile关键字
- 所以最完整的写法为
public class DCLSingleton {
// 单例
private static volatile DCLSingleton singleton = null;
// 私有构造方法
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
if (null == singleton) {
synchronized (DCLSingleton.class) {
if (null == singleton) {
singleton = new DCLSingleton();
}
}
}
return singleton;
}
}
- 解析:
首先用到idea的一个插件:可以方便的看到字节码文件
jclasslib Bytecode Viewer 使用方法可自行研究
0 aconst_null
1 getstatic #2 <org/dcl/DCLSingleton.singleton : Lorg/dcl/DCLSingleton;>
4 if_acmpne 39 (+35)
7 ldc #3 <org/dcl/DCLSingleton>
9 dup
10 astore_0
11 monitorenter
12 aconst_null
13 getstatic #2 <org/dcl/DCLSingleton.singleton : Lorg/dcl/DCLSingleton;>
16 if_acmpne 29 (+13)
19 new #3 <org/dcl/DCLSingleton>
22 dup
23 invokespecial #4 <org/dcl/DCLSingleton.<init> : ()V>
26 putstatic #2 <org/dcl/DCLSingleton.singleton : Lorg/dcl/DCLSingleton;>
29 aload_0
30 monitorexit
31 goto 39 (+8)
34 astore_1
35 aload_0
36 monitorexit
37 aload_1
38 athrow
39 getstatic #2 <org/dcl/DCLSingleton.singleton : Lorg/dcl/DCLSingleton;>
42 areturn
这里重点解析一下19 ~ 26行
其实19到26行就是new 单例对象的过程。
19 行:申请一块内存空间, 保存该对象(注意在此过程还没有对对象进行初始化,也就是调用构造方法)该对象只是占用一块内存空间,如果该单例中有变量或者对象此时是JVM虚拟机给的默认值
23行: 调用构造方法,对其进行初始化。
13,26行:就好比栈中的一个引用变量 getstatic是获得这个变量,putstatic是将这个对象指向这个变量。
如果按照正常的顺序确实是没有问题的,但是你不能保证它没有指令的重排:如果发生指令的重排,
但是可能会发生指令的重排,26行和23行可能会,交换顺序后为
19 new #3 <org/dcl/DCLSingleton>
26 putstatic #2 <org/dcl/DCLSingleton.singleton : Lorg/dcl/DCLSingleton;>
23 invokespecial #4 <org/dcl/DCLSingleton.<init> : ()V>
这是就会出现问题
比如说:如果有两个线程 A, B 线程A,执行完26后,cpu切换到B线程,这时B线程进行第一次判断就不为空,B线程拿到了一个没有初始化的对象。
如果这个单例是你的账户,可想而知。。。