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

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

单例(双重检查锁)


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修饰的,于是在该条语句的前后分别添加内存屏障。**防止了指令的重排。这样就解决了双锁检查单例的问题。





相关文章
|
14天前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
1月前
|
编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(上)
动态内存分配与管理详解(附加笔试题分析)
49 1
|
2月前
|
程序员 编译器 C++
【C++核心】C++内存分区模型分析
这篇文章详细解释了C++程序执行时内存的四个区域:代码区、全局区、栈区和堆区,以及如何在这些区域中分配和释放内存。
52 2
|
9天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
19天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
128 9
|
23天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
1月前
|
程序员 编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(下)
动态内存分配与管理详解(附加笔试题分析)(下)
46 2
|
1月前
|
存储 安全 Java
JVM锁的膨胀过程与锁内存变化解析
在Java虚拟机(JVM)中,锁机制是确保多线程环境下数据一致性和线程安全的重要手段。随着线程对共享资源的竞争程度不同,JVM中的锁会经历从低级到高级的膨胀过程,以适应不同的并发场景。本文将深入探讨JVM锁的膨胀过程,以及锁在内存中的变化。
40 1
|
1月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。