【单例深思】双重检测锁与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

STM32F103串口接收中断:USART_IT_IDLE和USART_IT_RXNE区别

2:如果是大于1个byte的数据,在不用DMA的情况下,同时使能USART_IT_RXNE,USART_IT_IDLE中断,每收到一个字节在中断中把数据存到缓存中,待收到USART_IT_IDLE中断时,收完整帧数据,然后再解析处理数据。比如:如上图在初始化时,同时开启了两个中断,如果传送8个字节的数据,就会产生8个USART_IT_RXNE,和1个USART_IT_IDLE中断。(如果接收到数据,在1个byte的时间内,没有收到新数据,就认为是数据传送完成,产生空闲USART_IT_IDLE中断)

weixin_66299220的博客 2399

Java中什么是双检锁

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

訾博(ZiBo)的博客 817

HDMI 2.1超高清传输背后的“隐形功臣”:深入浅出图解VESA DSC压缩核心流程

本文深入解析HDMI 2.1超高清传输中的VESA DSC压缩技术,揭示其如何在不损失画质的前提下实现带宽减半。通过切片并行处理和智能算法,DSC技术有效解决了8K/4K高帧率视频的传输瓶颈,成为现代显示设备不可或缺的“隐形功臣”。

weixin_30615767的博客 423

Solidworks在Windows 11上安装失败?这里有解决方案

通过上述步骤,你应该能够解决在Windows 11上安装Solidworks时遇到的问题。这个方法通过调整注册表的设置,提高了安装程序的权限,从而有效避免了安装过程中的权限障碍。当然,每个系统的具体情况可能有所不同,如果在尝试上述方法后仍然无法解决问题,建议联系Solidworks的技术支持团队,获取更专业的帮助。希望本文能够对你有所帮助,祝你工作顺利,设计无忧!

nntxthml的博客 4912

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

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

weixin_39158966的博客 882

瑞萨 RL78 开发环境实战:从 CS+ for CC 到 e² studio 高效开发

本文详细介绍了瑞萨RL78单片机开发环境的选择与配置,对比分析了CS+ for CC和e² studio的特点及适用场景。内容涵盖环境安装、工程创建、调试技巧、外设驱动开发和低功耗设计等实战经验,为开发者提供全面的RL78入门指南,帮助快速上手并高效开发。

4k5l6j7h8的博客 242

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

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

Ared 744

Jetson Nano(1)——笔记本电脑控制

笔记本电脑控制Jetson Nano没有显示屏如何控制Jetson Nano一、使用ssh客户端连接到Jetson Nano的命令端1.将电脑的网线口和jetson nano的网线口通过网线连接2.在控制面版中打开“更改配适器设置”3.查找jetson nano的ip地址4.利用putty链接二、远程桌面配置 没有显示屏如何控制Jetson Nano Jetson Nano相当于一个微型电脑,能快...

weixin_43619070的博客 1万+

腾讯会议视频过期了怎么办?亲测可用:用这个免费插件抢救你的会议录制与回放

本文提供了三种亲测有效的腾讯会议视频过期恢复方案,包括浏览器缓存提取、专业工具组合及预防性措施。特别推荐使用免费插件抢救过期会议录制与回放,帮助职场人解决重要视频资料丢失的难题。

weixin_33686714的博客 264

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

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

InkGenius 843

基于Vivado MIG IP核的DDR3读写实验(top_rom_ddr/ddr_top)

一、前言 关于Vivado MIG IP核详细配置可以参考我之前的文章:基于Vivado MIG IP核的DDR3控制器(DDR3_CONTROL) 关于MIG IP核的用户端的接口时序可以参考这篇文章:XILINX 的 MIG IP(非AXI4)接口时序以及控制 二、实验内容 本次实验的内容主要是通过MIG IP核向DDR3读写数据,DDR3的接口时序由ddr_top模块提供: ddr_top模块的数据来源是wr_fifo,wr_fifo的数据实际来自top_sd_photo模块(本实验仿真

耐心的小黑的博客 3027

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

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

VIP129370的博客 1333

1.4 KFD 设备文件的获取与驱动握手协议

本文分析了OpenClose模块中libhsakmt与KFD内核驱动建立通信的过程。首先通过hsaKmtOpenKFD()函数打开/dev/kfd设备文件,建立用户态与内核态的通信通道。随后通过AMDKFD_IOC_GET_VERSION ioctl命令完成版本握手,确认双方的接口兼容性。KFD驱动采用Major.Minor二级版本号体系,当前主要版本为1,从1.1演进到1.19不断添加新功能。整个过程构成了用户态与内核态之间的"建连协议",是所有后续GPU操作的基础前提。

图形/计算GPU的驱动与虚拟化技术分享园地 602

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

Java 单例模式——双检锁

双检锁: 双重检验锁。

Scp_blog的博客 958

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

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

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

杭电鬼先生交流中心 7072

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

VC中sqlite的使用

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

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

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

nancheng_wx的博客 423

双重检查锁定——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内存模型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程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些 对象时才进行初始化。此时,程序员可能会采用延迟初始化。但要正确实现线程安全的延迟初 始化需要一些技巧,否则很容易出现问题。比如,下面是非线程安全的延迟初始化对象的示例 代码。 public

7me 5428

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

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

今天吃了吗的博客 511

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、付费专栏及课程。

余额充值