通过 final 关键字来实现 双重检查(DCL) 时,为什么 局部变量 是必须的?

由单例模式的优化,引出的java线程数据同步和类加载顺序知识点总结 由单例模式的优化,引出的java线程数据同步和类加载顺序知识点总结 摘要 几种单例模式的优缺点及其改进优化 DCL失效问题的原因以及解决 java中线程同步关键字final和volatile java内存屏障 java类加载顺序总结 饿汉单例 //片段1 class Singleton1{ private stat 阅读详情

在 双重检查锁定(DCL)的wiki 中,对于 DCL提供了一种通过 final 关键字来实现的方式,源码如下:

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class Foo {
   private FinalWrapper<Helper> helperWrapper;

   public Helper getHelper() {
      FinalWrapper<Helper> wrapper = helperWrapper;

      if (wrapper == null) {
          synchronized(this) {
              if (helperWrapper == null) {
                  helperWrapper = new FinalWrapper<Helper>(new Helper());
              }
              wrapper = helperWrapper;
          }
      }
      return wrapper.value;
   }
}

同时,文章中有如下说明:
The local variable wrapper is required for correctness.

请问一下,为什么这个 局部变量 wrapper 是必须的?
我个人认为此变量是可有可无的,还请有识者指点一下。


本人回答如下:

第一次看到双重锁定检查还可以用final来实现,我试着回答一下这个问题,说一下自己的理解。

首先说一下基于volatile实现的双重锁定检查:

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();//instance为volatile,现在没问题了
            }
        }
        return instance;
    }
}

如果instance不声明为volatile类型的,上面代码中的双重锁定检查在多线程环境下是会有问题的。
主要问题在于多线程环境下,在一个线程中instance有可能在没有完全初始化之前(只初始化了一部分),就被其他线程使用了。 比如,instance刚被分配了内存空间,还没完成new Instance()的全部操作,其他线程在第一次对instance进行是否为空的判断的时候,结果是false(由于instance指向的是个内存地址,所以分配了内存空间之后,instance这个变量已经不为空),这个时候这个线程就会直接返回,然而instance变量指向的内存空间还没完成new Instance()的全部操作。 这样一来,一个没有被完全初始化的instance就会被使用。

上面我说的这种状况看起来不可能发生,毕竟从代码来看,这根本不可能啊。

初始化instance变量的伪代码如下所示:

memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址

之所以会发生上面我说的这种状况,是因为在一些编译器上存在指令排序,初始化过程可能被重排成这样:

memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//注意,此时对象还没有被初始化!
ctorInstance(memory); //2:初始化对象

而volatile存在的意义就在于禁止这种重排!要想详细了解一下指令重排,可以参考一下这里。

再来说一下final域的重排规则:

写final的重排规则:

JMM禁止编译器把final域的写重排序到构造函数之外。
编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
也就是说:写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了。

读final的重排规则:

在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。
也就是说:读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。

如果final域是引用类型,那么增加如下约束:

在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
(个人觉得基本意思也就是确保在构造函数外把这个被构造对象的引用赋值给一个引用变量之前,final域已经完全初始化并且赋值给了当前构造对象的成员域,至于初始化和赋值这两个操作则不确保先后顺序。)
具体解释可以参考这里

再回到这个问题本身:

 public class Foo {
   private FinalWrapper<Helper> helperWrapper;

   public Helper getHelper() {
      FinalWrapper<Helper> wrapper = helperWrapper;

      if (wrapper == null) {
          synchronized(this) {
              if (helperWrapper == null) {
                  helperWrapper = new FinalWrapper<Helper>(new Helper());
              }
              wrapper = helperWrapper;//此处增加了对helperWrapper的引用,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了。
              //所以此处增加一次对helperWrapper的引用,可以确保FinalWrapper中的value已经成功初始化。
              //如果此处没有赋值给本地变量,final域的写操作保证不能生效,那么getHelper()方法并不能保证,被return的value已经被正确初始化过。
          }
      }
      return wrapper.value;
      //读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。所以此处的读取操作确保读value之前,会读取wrapper这个引用变量指向的对象,
      //而写操作又可以确保在对象可见之前,final域已经正确初始化了。所以此处肯定会读取到正确的value。
      //如果读final域不做这个保证,那么对value的读取操作有可能会被重排到对wrapper赋值之前,有可能读取到不正确的value值。
   }
}

主要问题在于这些关键字对指令重排的保证,个人的一个理解是这样的,欢迎讨论。

本篇文章内容源自于对Segmentfault一个问题的回答。

Linux -- 图形界面配置Samba;system-config-samba工具 图形界面配置Samba6.6.1 system-config-sambasystem-config-samba是Red Hat公司专门为Samba服务器管理编写的图形界面的管理工具,通过system-config-samba几乎可以完成对Samba服务器的所有配置,所以在只希望通过Samba服务器实现一些简单的资源共享时,system-config-samba是一个不错的选... 阅读详情

相关推荐

9 海康视觉平台VisionMaster 上手系列:典型案例之螺丝机案例

海康视觉平台VisionMaster 上手系列:典型案例之螺丝机案例 快速入门 工业机器视觉 机器视觉

HuangGong2022的博客 7168

Java final关键字与类设计模式实战解析

final关键字是Java语言中实现不可变性的核心机制,通过修饰变量、方法和类三个层级保证对象状态安全。在并发编程领域,final的happens-before语义能有效防止指令重排序,与volatile共同构成线程安全的基础保障。设计模式层面,单例模式通过final与静态机制的组合,确保全局唯一实例的创建,而枚举类型则天然具备final特性,成为实现状态机和策略模式的最佳载体。在电商交易、金融支付等高并发场景中,合理运用final修饰核心业务对象,既能提升JVM优化空间,又能规避线程安全问题。本文通过五种

weixin_33226548的博客 215

脑机接口(BCI)从原理到开发实战 09|SSVEP应用:通过视觉刺激控制设备

本文介绍了稳态视觉诱发电位(SSVEP)脑机接口技术。SSVEP是一种高效的非侵入式BCI范式,当用户注视特定频率闪烁的视觉刺激时,大脑枕叶会产生对应的节律性响应。文章详细阐述了SSVEP的神经机制、刺激范式设计、数据预处理方法,重点讲解了典型相关分析(CCA)这一核心识别算法。相比运动想象和P300范式,SSVEP具有信息传输率高(60-100 bits/min)、信号稳定、无需训练等优势,在高速拼写、设备控制等领域应用广泛。文章还提供了完整的电极选择、滤波、分段等预处理流程,以及CCA和滤波器组CCA的

小明的博客 1350

php 双重检查,java,final_通过 final 关键字来实现 双重检查(DCL) 时,为什么 局部变量 是必须的?,java,final,wrapper,dcl,双重检查 - phpStud...

通过 final 关键字来实现 双重检查(DCL) 时,为什么 局部变量 是必须的?在双重检查锁定(DCL)的wiki中,对于 DCL提供了一种通过 final 关键字来实现的方式,源码如下:public class FinalWrapper {public final T value;public FinalWrapper(T value) {this.value = valu...

weixin_33785596的博客 147

别再只调包了!深入理解机器学习虚拟筛选中的分子指纹(ECFP/Morgan)

本文深入探讨了ECFP/Morgan分子指纹在机器学习虚拟筛选中的应用与优化策略。通过解析半径参数与比特长度的化学意义,揭示了如何通过参数调优提升模型性能20%以上,特别适用于药物发现领域的高通量筛选。文章还提供了工程实践中的参数优化策略和多指纹融合技术,为开发者提供从理论到实践的全面指导。

weixin_33704234的博客 829

聚宽策略实盘新路径:不开通QMT , 小资金自动化交易解决方案

聚宽策略实盘新路径:不开通QMT , 小资金自动化交易解决方案

u011111246的博客 1466

DCL,volatile,Final

Double check lock模式的典型代码:     public class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { i...

asticx 208

深信服PT1-AF实验考试

深信服PT1-AF实验考试实验练习可以去深信服的在线实验平台(链接如下):深信服在线实验平台 (sangfor.com.cn)实验考试其实就是把所有的每一部分实验综合了起来以下内容仅做参考,不确保一样。

m0_72019240的博客 7106

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

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

记忆力不好的博客 8万+

WorldQuant Brain Alpha 生成器

一个自动化工具,通过参数优化和回测来挖掘高质量的量化交易 Alpha 因子,并自动提交符合标准的因子。

weixin_37582383的博客 1万+

Android BLE扫描指南:解决设备发现问题

假设你正在开发一个MAUI应用,用于扫描并连接到BLE设备。你的代码在Windows上运行得很好,但在Android上却无法发现任何设备。尽管你已经在中声明了必要的权限,并且在运行时请求这些权限,扫描还是无法工作。

m0_62153576的博客 224

双重检查锁定及单例模式

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

snakeqi的专栏 1961

FPGA和USB3.0通信-USB3.0 PHY介绍

可以和FPGA搭配使用的USB3.0 PHY芯片通过之前的介绍<FPGA和USB3.0通信-USB3.0协议介绍USB3.0 PHY简介首先我们需要了解PHY具体完成哪些工作以及我利...

OpenFPGA的博客 4940

【java】双重检查加锁到底锁了个啥?

本文系统分析了多线程环境下单例模式的双重检查加锁(DCL)机制。重点阐述了锁对象的本质是对象实例而非引用变量,解释了为什么锁类对象和堆对象各有优劣:前者简单但粒度大,后者专用但稍复杂。文章明确了不能锁this或instance的原因,强调了static final的必要性,指出DCL仅保证初始化安全而非方法并发安全。最后用"工牌仲裁器"比喻形象说明DCL的核心作用是在首次初始化时提供互斥保护,后续访问则无需加锁。

ganshenml的专栏 1302

Devexpress16.2.6破解工具

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

老版QQ原生表情gif动态

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

DCL 为什么还需要 volatile

DCL Double-checked locking 双重检查锁定模式 双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。 为多线程环境中的单例模式实现“惰性初始化”。 // Broken multithreaded version // "Double-Checked Locking" idiom class Foo { private Helper helper = null; public Helper getHelp

晨猫 1423

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

final关键字-单例模式-静态代码块

双重检查锁定是一种优化技术,用于实现懒汉式的单例模式。其核心是在非线程安全的检查之后再加一个线程安全的检查,从而保证实例化过程的线程安全性和性能。

m0_68319667的博客 2256

单例与双重检测

public class Singleton { private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { ...

u011552955的博客 188

【关键字】Java核心关键字:final、static、volatile、synchronized、transient(附《面试高频考点》)

Java五大核心关键字精讲:final(不可变性)、static(类级共享)、volatile(可见性+禁重排)、synchronized(原子性/可见性/有序性)和 transient(非序列化)。涵盖原理、场景、多线程与序列化特性,直击面试高频考点。

小江的记录本 466

常见线程安全问题之Double Checked Locking

最简单的解决方式是读操作也加锁,如果性能达不到要求,也可以像本节一样使用 volatile,但我个人不建议这么用,因为有太多细节需要考虑,可以使用 JUC 中的。问题不仅仅在于写的一方,即使 helper 被正确初始化并赋值,由于另一个线程所在的 CPU 可能会从缓存中读取 helper 的值,如果 helper 的新值还没有被更新到缓存中,则读取的值可能还是。因为 64 位的操作不是原子的,于是可能造成前后 32 位不是一起写入内存的,而另一个线程只读取先写入的 32 位,读到的结果不正确。

彩虹地带 1151

Java final关键字深度解析:从常量定义到并发安全的不可变魔法

在Java编程中,不可变性是构建健壮、可预测代码的核心概念之一。其原理在于通过语言机制限制对象状态的变更,从而消除副作用和竞态条件。这一特性在并发编程中尤为重要,能够简化线程安全设计,避免数据竞争。从技术价值看,不可变性不仅提升了代码的可读性和可维护性,还为编译器优化(如内联和常量传播)提供了明确保证。在实际应用场景中,它广泛用于配置管理、常量定义、工具类设计以及构建线程安全的共享对象。本文以Java中的final关键字为切入点,结合静态常量和不可变对象等热词,深入探讨如何通过final实现不同层次的不可变

weixin_30301449的博客 387

1.4 面试题 双重检查锁

这段代码是双重检查锁定的单例模式,利用。

你是人间四月天 303

【Java EE】volatile关键字

volatile关键字解决的两个问题、无原子性以及底层原理

465

DCL单例模式

dcl单例模式:懒汉式基础上加入并发控制,保证多线程下对外只存在一个单例。 public class DoubleCheckingLocking { private static DoubleCheckingLocking dcl; private DoubleCheckingLocking(){ } public static DoubleCheck...

finalblue的博客 267

Java线程安全单例的4种实现与volatile原理

单例模式是面向对象编程中基础但关键的设计模式,其核心在于保证全局唯一实例。在多线程环境下,该目标面临原子性与可见性的双重挑战——竞态条件可能导致多个实例被创建,而内存可见性缺失则可能使线程读取到未完全初始化的对象。Java通过synchronized提供互斥与可见性保障,但存在性能瓶颈;volatile则通过禁止指令重排序和插入内存屏障,精准解决对象构造过程中的发布问题。结合DCL(双重检查锁定)、静态内部类、枚举及饿汉式等主流方案,开发者需根据启动开销、延迟加载需求与JVM机制选择最优解。本文深入剖析vo

weixin_30861797的博客 335

concurrenthashmap是什么锁_「Java并发」深入剖析ConcurrentHashMap

简介ConcurrentHashMap实现了ConcurrentMap接口,先看看ConcurrentMap接口的文档说明:1.理解JAVA存储模型(JMM)的发生-之前规则。2)经过前面的了解,下面我们用Happens-Before规则理解一个经典问题:双重检测锁(DCL)为什么在java中不适用?备注1:备注2:后续补充:深入剖析ConcurrentHashMap简介ConcurrentHas...

weixin_42127775的博客 269

java 并行 用happen-before规划重新审视DCL

摘自 http://lifethinker.iteye.com/blog/260515 用happen-before规则重新审视DCL 博客分类: java 多线程虚拟机数据结构YoutubeSQL 编写Java多线程程序一直以来都是一件十分困难的事,多线程程序的bug很难测试,DCL(Double Check Lock)就是一个典型,因此对多线程安全的理论分...

weixin_30384031的博客 104
上一篇: 吐槽一下最近遇到的一些交流上面的事情
下一篇: 面试代码
java_4_ever
博客等级 码龄16年 8粉丝 29原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值