【单例深思】双重检测锁与Java内存模型

Java ~ 双重检查锁(DCL)的原理与失效原因 前言 为了保证线程的安全性,往往要以牺牲性能为代价。为了兼得二者,前人进行了多番尝试,也确实创造出诸多有效方案,双重检查锁就是其中的一种。 双重检查锁(DCL:Double Check Lock)。令人哭笑不得的是,其闻名原因不是因为有效性,而是行业标杆级的错误性。双重检查锁同时体现了同步中的独占性与可见性同等的重要性,因此成为多线程学习中必学的经典案例。 一,双重检查锁(DCL)的原理 我们以DCL的方式来实现单例模式作为学习举例。 @Data public class Singleton { 阅读详情
双重检测锁  使用粒度较小的锁( 缩小锁的范围) 解决了  懒汉式改进版  中存在的性能问题,以提高并发量。

双重检测锁实现如下:

public class Singleton {                        // 1
    private static Singleton singleton;         // 2
    private Singleton() {}                      // 3
    public static Singleton getSingleton() {    // 4
        if (singleton == null) {                // 5
            synchronized (Singleton.class) {    // 6
                if (singleton == null) {        // 7
                   singleton = new Singleton(); // 8
                }                               // 9
            }                                   // 10
        }                                       // 11
        return singleton;                       // 12
    }                                           // 13
 }                                              // 14  

该实现改变了  synchronized  的作用范围,将同步方法改为了同步代码块。并增加了一次   if  ( singleton  ==  null )  判断。

第一次检测的作用是什么?

如果第一次检测 singleton   为 null ,则会进入同步代码块,进行对象实例化操作。这个操作只会进行一次,以后 执行 getSingleton() 
第一次检测  singleton   不为 null ,那么就不需要执行下面的加锁和实例化操作,直接返回实例对象,因此可以大幅降低 synchronized  带来的性能开销。因此,第一次检测就是为了对象实例化之后可以直接返回实例对象。

那么为什么还需要第二次检测?

当 singleton   为 null 时, 可能会有多个线程一起进入同步块外的 if ,假设线程A先获得  Singleton. class 锁,并完成了初始化操作,此时线程B再获得 Singleton. class 锁,就会再次执行实例化操作,就会生成多个实例了。因此有需要再次判断对象是否为 null 空,以防止出现实例化多个对象的错误。

这个修复了  懒汉式改进版  中存在的性能问题,但真的完美了吗?

天下没有完美的事,这个优化也不例外,这里还有一个致命的错误,会让之前的努力前功尽弃! 

错误的根源在于 Java内存模型  中一个很重要的概念 —— 指令重排序。
简单地说,是编译器、处理器以及运行时等可能对操作的执行顺序进行一些意想不到的调整,以提高执行效率。下面是一个概念模型:

上面的代码第8行, singleton  =  new  Singleton(); 创建了一个对象,别看一个简单的 new ,但是幕后要做的工作很多很复杂。当调用静态方法 getSingleton() 时,就会触发类加载,完成类加载后,实例化对象时,简单来说可以分解为以下三个步骤:

  1. 在堆上给 singleton 分配内存。
  2. 调用 Singleton() 的构造函数来初始化成员变量,此时类变量已经完成了初始化。
  3. 将Java 栈本地变量表中的 singleton 引用指向Java堆中为对象分配的内存空间,执行完这步 singleton 就为非 null 了。

上面三个步骤中的 2 和3 之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的)。2 和 3之间重排序之后会使得初始化放到第三步进行,也就是说尽管   singleton   非 null 了,但是这个对象还没有初始化,是一个不可用的对象。

我们怎么知道什么时候会重排序, 怎么解决重排序问题?

重排序问题既然是为了提高执行效率才采取的措施,那么必须先保证执行的结果和未重排序时执行的结果相同,否则就舍本逐末了。
这就引出了Java 内存模型中 另外一个概念 :as-if-serial语义。

as-if-serial语义 的意思指:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、运行时和处理器都必须遵守 as-if-serial语义 。

为了遵守 as-if-serial语义 ,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例:

        double pi  = 3.14;        //A
        double r   = 1.0;         //B
        double area = pi * r * r; //C  

上面三个操作的数据依赖关系如下图所示:


如上图所示,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面,C 如果排到A和B的前面,程序的结果将会被改变。
但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
下图是该程序的两种执行顺序:

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、运行时和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。 as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。也就是说单线程下的 new操作尽管可能进行2和3操作重排序,但是不会影响结果。

重排序对多线程的影响?

这正是我们的双重检测锁实现中遇到的问题,前面说到这种实现可能会让我得到一个 尚未初始化的对象,这就是重排序对多线程的影响之一。

怎么解决多线程中重排序问题?

我们可以使用  volatile 关键字,只需要用这个关键字声明 singleton 变量即可,实现如下:

public   class  Singleton {
    private static volatile Singleton singleton;
    private Singleton() {}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}


要介绍 volatile   关键字,先要简单介绍 Java内存模型(JMM) 中的相关概念。

内存可见性 :一个线程对 共享变量 值得修改,能够及时地被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。

Java 内存模型(Java Memory Model)描述了Java 程序中各种变量( 线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

主内存:所有的变量都存储在主内存中。
工作内存:没个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)。

 
两条规定:
  1. 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
  2. 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值得传递需要通过主内存来完成。

共享变量可见性实现原理:

线程1对共享变量的修改要想被线程2及时看到,必须经过如下2个步骤:
  1. 把工作内存1中更新过的共享变量刷新到主内存中
  2. 把主内存中最新的共享变量的值更新到工作内存2中

要实现共享变量的可见性,必须保证两点:
  1. 线程修改后的共享变量值能够及时从工作内存刷新到主内存中
  2. 其他线程能够及时把共享变量的最新值从朱内存更新到自己的工作内存中

 Java 语言层面支持的可见性实现方式:
  1. synchronized
  2. volatile

volatile 关键字能够保证volatile变量的可见性,不能保证volatile变量复合操作的原子性。

volatile如何实现内存可见性

深入来说:通过加入内存屏障和禁止重排序优化来实现的。
  • 对volatile变量执行写操作时,会在写操作后加入一条 store 屏障指令
  • 对volatile变量执行读操作时,会在读操作后加入一条 load 屏障指令

通俗来说: volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总会看到该变量的最新值。

所以, volatile变量 singleton 就能禁止重排序,从而解决了 双重检测锁中的问题了。

不过 volatile 语义在Java 1.5才得到增强,所以在通用性上有些限制。

参考:
http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization
http://www.infoq.com/cn/articles/java-memory-model-2

Java中双重检查锁(double checked locking) 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。双重检查锁(Double-Check Locking),顾名思义,通过两次检查,并基于加锁机制,实现某个功能。// 此处如果有多个执行流同时进入,会造成多次初始化// 第1次,一般性检查,但是有并发隐患:可能有多执行流同时进入改处// 此处第2次检查,为了防止后续多执行流并发时,后续获取同步锁的执行流,不会再次初始化Singleton对象。 阅读详情

相关推荐

Java并发编程中双重检查锁漏洞

但是Java虚拟机实际上会做一些优化,重新排列一些代码指令。重排后的顺序可能会变成: 1, 3, 2, 4, 5,这样在多线程的情况下也会创建多个实例。双重检查锁的机制不仅保证了线程安全,而且相比直接锁,提高了执行效率,节省了内存空间。答:您可以将volatile关键字添加到 的定义中INSTANCE。关键字可以保证多线程的volatile可见性但不保证原子性,也可以禁止指令重排序。你希望按1, 2, 3, 4, 金额 5顺序执行。

知识分享官 433

Java双检锁模式

双检锁单例模式通过两次判空检查实现线程安全,但存在指令重排序风险。对象创建过程包含内存分配、初始化和地址返回三步,其中后两步可能被重排序,导致返回未初始化对象。volatile关键字通过插入内存屏障防止指令重排序:写操作前后和读操作后插入屏障,确保多线程环境下操作顺序的一致性,从而避免返回未完成初始化的实例。这种机制保证了双检锁单例模式在多线程环境下的正确性。

纯洁的小魔鬼 3221

Unity Addressable资源缓存路径自定义指南:告别C盘爆满烦恼

本文详细介绍了如何自定义Unity Addressable系统的资源缓存路径,解决其默认存储在C盘导致的磁盘空间不足问题。通过配置CacheInitializationSettings并编写编辑器脚本,开发者可以将缓存迁移至项目目录,实现资源的集中管理,提升团队协作效率与开发环境一致性。

pytorch8learner的博客 474

Java中什么是双检锁

在Java中,双重检查锁定(Double-Checked Locking)是一种多线程编程中用来减少同步的开销的技术。双检锁的主要用途是**在创建单例模式时,确保只有一个实例被创建,尤其是在懒汉式单例模式中,**它被大量使用。

訾博(ZiBo)的博客 817

MITRE ATT&CK超详细学习笔记-01(背景,术语,案例)

MITRE是一个向美国政府提供系统工程、研究开发和信息技术支持的非营利性组织,于1958年从麻省理工学院林肯实验室分离出来后参与了许多最高机密的政府项目,开展了大量的网络安全实践。例如,MITRE公司在1999年发起了常见披露漏洞项目(CVE,CommonVulnera-bilitiesandExposures)并维护至今。其后,MITRE公司还维护了常见缺陷列表(CWE,CommonWeaknessEnumeration)这个安全漏洞词典。.........

Zichel77的博客 1万+

别再手动拆解字节了!用博图这个功能指令,5分钟搞定ARRAY转BOOL数组

本文详细介绍了西门子博图中`ARRAY_TO_BOOL`功能指令的高效应用,帮助工程师快速实现ARRAY到BOOL数组的转换。通过实战案例和参数配置指南,展示了如何5分钟内完成传统方法需半小时的任务,大幅提升PLC编程效率,特别适用于通信报文解析和设备状态监控。

weixin_42653691的博客 260

Java里面为什么搞了双重检查锁,写完这篇文章终于真相大白了

延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于 volatile 的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。

weixin_39158966的博客 882

IAR编译相同工程生产hex文件checksum不一致问题

IAR Checksum

xqw19891201的博客 1382

高并发压测第3小时:面试官质疑JVM调优方案,应届生现场手撕Java内存模型

在高并发压测的第3个小时,面试官突然质疑应届生提出的JVM调优方案。面对质疑,应届生不仅详细解释了方案背后的原理,还现场手撕Java内存模型(JMM),深入分析了双重检测锁的实现细节,最终说服面试官并赢得信任。

Ared 744

【FPGA】Robei EDA 的使用(7)—— Robei文件上板试验

#沐朝露兮以自华,觅大道兮以求成 我又滚过来了,研究了20多天RISCV,结果最后因为硬件问题导致无法在比赛中使用RISCV CPU,虽然已经临近比赛后期了,非常的崩溃,但是也没有什么办法。唉,一切不过是从头再来罢了。(写写博客舒缓我心中苦闷) ————————————————进入正文的分割线———————————————— 今天介绍一下如何利用 Robei EDA 生成文件,导入vivado中,综合下载到开发板里。拿我一个PWM控制舵机的代码为例。 Robei EDA设置 假设代码模块(model文件)经

Ninquelote的博客 2561

阻抗匹配与史密斯(Smith)圆图

在找到点B和B'后,我们就能够测量A'到B和B'到D的弧长,前者就是C的归一化电纳值,后者为L的归一化电抗值。当然,表面上看新的点好像是一个不同的阻抗,实际上Z和1/Y表示的是同一个元件(这个新值在圆图上呈现为一个不同的点,而且反射系数也不相同,依次类推)。出现这种情况的原因是我们的图形本身是一个阻抗图,而新的点代表的是一个导纳。这个方程是在复平面(Γr, Γi)上、圆的参数方程(x - a)² + (y - b)² = R²,它以[r/(r + 1), 0]为圆心,半径为1/(1 + r)。

qq_55759599的博客 3961

【单例深思】Java 单例模式全解

什么是单例模式? 单例模式 是一种对象创建模式,它用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例,并提供一个访问它的全局访问点。 单例模式的用途? 用于创建只需要一个实例的场景。 怎么实现单例模式? 单例模式的实现中有几个共同特点: 使用 private 声明类构造函数,这样就不能通过new 声明这个

InkGenius 843

机械式激光雷达实现原理以及特性总结

本文介绍了机械式激光雷达的技术原理和核心特性。该技术通过机械旋转结构实现360°三维扫描,采用飞行时间法测距并生成点云数据。核心优势包括全向扫描、长距高精度和成熟度高,但存在体积大、成本高、可靠性受限等局限。典型应用于自动驾驶测试车、测绘建模等领域。与固态激光雷达相比,机械式在视场角和探测距离方面有优势,但在体积、成本和可靠性方面处于劣势。该技术目前主要应用于对成本不敏感的专业场景。

start_up_go的博客 262

Java 单例模式——双检锁

双检锁: 双重检验锁。

Scp_blog的博客 958

数据分享|1961-2017年中国0.25°×0.25° 逐日地表水文数据集(VIC-CN05.1)

其中,全球径流中心(University of New Hampshire and Global Runoff Data Centre, UNH/GRDC)的复合产流场集成了观测的河道径流和水量平衡模式模拟的地表和地下径流,常被用来评估模拟的径流。在长白山、海北灌丛和千烟洲通量站,模拟与观测的逐月潜热通量的平均NSE为0.61。VIC-CN05.1受益于较准确的大气驱动场、先进的地表水文参数化方案以及更真实丰富的土壤信息,可作为中国水文气象研究的参照数据,用于陆面模式的评估和长期的趋势分析等。

J_Giser的博客 1830

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

Java中的双重检查锁机制模式(double checked locking)

2021年的金三银四一眨眼就到了,对于很多人来说是跳槽的好机会,大厂面试远没有我们想的那么困难,摆好心态,做好准备,你也可以的。另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。BAT面试经验实战系列:Spring全家桶+Redis等其他相关的电子书:源码+调优其他相关的电子书:源码+调优。

VIP129370的博客 1333

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

JAVA的单例模式双重锁检查机制

目的是为了禁止指令重排序,防止错误发生;

nancheng_wx的博客 423

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Java中的双重检查(Double-Check)详解

Java中的双重检查(Double-Check)详解 文章参考: https://www.cnblogs.com/xiemingjun/p/9628903.html https://zhuanlan.zhihu.com/p/31167297 双重检查锁和单例类 单例类在Java开发者中非常常用,但是它给初级开发者们造成了很多挑战。他们所面对的其中一个关键挑战是,怎样确保单例类的行为是单例?也就是说,无论任何原因,如何防止单例类有多个实例。在整个应用生命周期中,要保证只有一个单例类的实例被创建,双重检查锁(D

杭电鬼先生交流中心 7072

双重检查锁定——Java底层双重锁问题分析

双重检查锁定(DCL)是一种兼顾性能与线程安全的单例模式实现方式。其核心是在首次调用时延迟初始化实例,并通过两次判空检查避免重复加锁。关键问题在于new Singleton()非原子操作,可能因指令重排序导致返回未完全初始化的对象。解决方法是为单例实例添加volatile修饰,禁止JVM重排序并保证可见性。典型实现包含:1)首次无锁检查提升性能;2)同步块内二次检查防止重复创建;3)volatile确保初始化顺序。相比全方法加锁,DCL在保证线程安全的同时显著提升性能,是多线程环境下单例模式的优选方案。

阿常的博客 824

从Java内存模型讲双重检查锁原理

转载: 超赞的两篇文章 https://www.jianshu.com/p/4586cb11c62d https://www.cnblogs.com/xz816111/p/8470048.html

Angus的博客 216

java内存模型 双重检查锁定与延迟初始化

目录 双重检查锁定的由来 问题的根源 基于volatile的解决方案 基于类初始化的解决方案 双重检查锁定的由来 在Java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些 对象时才进行初始化。此时,程序员可能会采用延迟初始化。但要正确实现线程安全的延迟初 始化需要一些技巧,否则很容易出现问题。比如,下面是非线程安全的延迟初始化对象的示例 代码。 在Uns...

今天吃了吗的博客 511

Java内存模型之双重检查锁定与延迟初始化

前言 在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。双 重检查锁定是常见的延迟初始化技术,但它是一个错误的用法。本文将分析双重检查锁定的 错误根源,以及两种线程安全的延迟初始化方案。 双重检查锁定的由来 在Java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些 对象时才进行初始化。此时,程序员可能会采用延迟初始化。但要正确实现线程安全的延迟初 始化需要一些技巧,否则很容易出现问题。比如,下面是非线程安全的延迟初始化对象的示例 代码。 public

7me 5427

Java内存模型FAQ(十一)新的内存模型是否修复了双重锁检查问题?

臭名昭著的双重锁检查(也叫多线程单例模式)是一个骗人的把戏,它用来支持lazy初始化,同时避免过度使用同步。在非常早的JVM中,同步非常慢,开发人员非常希望删掉它。双重锁检查代码如下: // double-checked-locking - don't do this! private static Something instance = null; public Something getInstance() { if (instance == null) { synchronize...

weixin_51393513的博客 153

Java单例模式中双重检查锁的问题

单例创建模式是一个通用的编程习语。和多线程一起使用时,必需使用某种类型的同步。在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查锁定习语有效。它偶尔会失败,而不是总失败。此外,它失败的原因并不明显,还包含 Java 内存模型的一些隐秘细节。这些事实将导致代

记忆力不好的博客 8万+
上一篇: 【单例深思】懒汉式改进版与内置锁
下一篇: 【单例深思】静态内部类实现详解
TaiSung
博客等级 码龄15年 49粉丝 132原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值