双重检查锁单例与内存屏障分析

简介: 双重检查锁单例与内存屏障分析

单例(双重检查锁)


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();是做了以下的三件事的:


  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将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 读与下面的普通写重排序


8375de4bacf146bab49c8301539e097d.png


JVM规定volatile需要实现的内存屏障


e0b8be4b63294336886bd8fbb21c52da.png


分析


首先在锁机制下,当一条线程执行锁中的代码时,其他需要锁的线程全部就被挡在了锁的外部等待。该线程进入锁内,执行到instance = new DoubleCheckLockSingleton(); **发现instance变量是被volatile修饰的,于是在该条语句的前后分别添加内存屏障。**防止了指令的重排。这样就解决了双锁检查单例的问题。





相关文章
|
2月前
|
监控 Java
如何使用VisualVM分析内存泄漏?具体流程看这里
如何使用VisualVM分析内存泄漏?具体流程看这里
|
3月前
|
安全 Linux 编译器
内存泄漏检测组件的分析与实现(linux c)-mtrace工具使用
内存泄漏产生原因 在堆上使用malloc/remalloc/calloc分配了内存空间,但是没有使用free释放对应的空间。
76 0
|
3月前
3.1.2 内存池的实现与场景分析
3.1.2 内存池的实现与场景分析
|
1月前
|
算法 Java C++
【C/C++ 内存知识扩展】内存不足的可能性分析
【C/C++ 内存知识扩展】内存不足的可能性分析
12 0
|
1月前
|
缓存 Java
java使用MAT进行内存分析
java使用MAT进行内存分析
|
1月前
|
存储 缓存 Linux
嵌入式Linux中内存管理详解分析
嵌入式Linux中内存管理详解分析
39 0
|
2月前
|
存储 缓存 算法
Golang高性能内存缓存库BigCache设计与分析
【2月更文挑战第4天】分析Golang高性能内存缓存库BigCache设计
73 0
|
2月前
|
存储 监控 Java
JVM内存泄漏的分析与解决方案
JVM内存泄漏的分析与解决方案
|
2月前
|
测试技术 iOS开发
用Instruments分析内存使用情况
用Instruments分析内存使用情况
|
3月前
|
存储 Java 数据挖掘
来聊聊ThreadLocal内存泄露分析
来聊聊ThreadLocal内存泄露分析