单例(双重检查锁)
public class DoubleCheckLockSingleton { private static DoubleCheckLockSingleton instance = null; private DoubleCheckLockSingleton(){ } public static DoubleCheckLockSingleton getInstance() { if (instance == null){ /** * 10 monitorenter * 11 getstatic #2 <DoubleCheckLockSingleton.instance : LDoubleCheckLockSingleton;>//获取静态变量 * 14 ifnonnull 27 (+13)//判空 * 17 new #3 <DoubleCheckLockSingleton>//如果等于null,创建对象 * 20 dup * 21 invokespecial #4 <DoubleCheckLockSingleton.<init> : ()V>//对象的初始化 * 24 putstatic #2 <DoubleCheckLockSingleton.instance : LDoubleCheckLockSingleton;>//给变量赋值 * 27 aload_0 * 28 monitorexit */ synchronized (DoubleCheckLockSingleton.class){ if (instance == null){ instance = new DoubleCheckLockSingleton(); } } } return instance; } public static void main(String[] args) { DoubleCheckLockSingleton doubleCheckLockSingleton = DoubleCheckLockSingleton.getInstance(); } }
程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。注意上面的代码中属性是没有加volatile关键字的,是有可能发生指令重排的,
对于JVM来说:instance=new DoubleCheckLockSingleton();是做了以下的三件事的:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
对面上面的这段代码来说
JVM执行到instance=new DoubleCheckLockSingleton()时,可能的实行顺序就是1–>2–>3或1–>3–2,再多线程的情况下上面的两种情况都是有可能出现的。如果是前者自然是没有什么问题,但是如果出现后者那么就没有按照我们希望的循序执行。这样会出现已经重复创建对象的情况,也就没有达到我们单例的设计原则。
从更底层的方式来思考双锁出现的问题
这里我们使用idea插件jclasslib来分析(字节码阅读器)
主要看下面这部分:
/** * 10 monitorenter * 11 getstatic #2 <DoubleCheckLockSingleton.instance : LDoubleCheckLockSingleton;>//获取静态变量 * 14 ifnonnull 27 (+13)//判空 * 17 new #3 <DoubleCheckLockSingleton>//如果等于null,创建对象 * 20 dup * 21 invokespecial #4 <DoubleCheckLockSingleton.<init> : ()V>//对象的初始化 * 24 putstatic #2 <DoubleCheckLockSingleton.instance : LDoubleCheckLockSingleton;>//给变量赋值 * 27 aload_0 * 28 monitorexit */
高并发下,若全按照上面的顺序来执行就和我们期望的一样了,但是不然。编号21和24的顺序是不确定的,也就是存在半初始化问题,也就是24执行在前,21后。首先在锁机制下,当一条线程执行锁中的代码时,其他需要锁的线程全部就被挡在了锁的外部等待。该线程进入锁内,在对象没有真正的初始化时就对变量进行了赋值(此时对象里面的值都是堆中jvm给的默认值),外部的线程执行到 if (instance == null),发现不为空,立即返回该对象。那么线程就拿到了一个错误的对象
单例双重检查锁问题的解决
添加volatile
private static volatile DoubleCheckLockSingleton instance = null;
分析之前先补充知识:
volatile 的底层实现是通过插入内存屏障实现(C++,汇编实现)。
- 每个 volatile 写操作前面插入一个 StoreStore 屏障
- 每个 volatile 写操作后面插入一个 StoreLoad 屏障
- 每个 volatile 读操作后面插入一个 LoadLoad 屏障
- 每个 volatile 读操作后面插入一个 LoadStore 屏障
内存屏障
- StoreStore 屏障可以保证在 volatile 写之前,其前面的所有普通写操作都已经刷新到主内存中
- StoreLoad 屏障的作用是避免 volatile 写与后面可能有的 volatile 读/写操作重排序
- LoadLoad 屏障用来禁止处理器把上面的 volatile 读与下面的普通读重排序
- LoadStore 屏障用来禁止处理器把上面的 volatile 读与下面的普通写重排序
JVM规定volatile需要实现的内存屏障
分析、
首先在锁机制下,当一条线程执行锁中的代码时,其他需要锁的线程全部就被挡在了锁的外部等待。该线程进入锁内,执行到instance = new DoubleCheckLockSingleton(); **发现instance变量是被volatile修饰的,于是在该条语句的前后分别添加内存屏障。**防止了指令的重排。这样就解决了双锁检查单例的问题。