当前位置: 首页 > news >正文

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

创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家!


双重锁定检查(Double Checked Locking,下称 DCL)是并发下实现懒加载的一个模式,在实现单例模式时很常见,但是要正确实现 DCL,其中涉及到的细节和知识是非常琐碎的,我们这里按照 The "Double-Checked Locking is Broken" Declaration 文章的脉络,结合前几章学习的知识,尝试理解这些知识点。

(这章属于“骚操作”的内容。)

初次尝试

上节中说过 Lazy Initialization,我们的目标是在获取某个实例时只初始化一次,在单线程语境中,我们会这么实现:

class Foo {private Helper helper = null;public Helper getHelper() {if (helper == null)helper = new Helper();return helper;}// other functions and members...
}

但是我们知道这个版本在多线程下是有问题的,因为对 helper 和检查和赋值不是原子的,有可能多个线程同时满足了 if (helper == null) 的判断,最终多个线程都执行了 helper = new Helper 的操作。一个简单的方法是加锁:

class Foo {private Helper helper = null;public synchronized Helper getHelper() {if (helper == null)helper = new Helper();return helper;}// other functions and members...
}

注意代码里的 synchronized。这个代码能正确运行,但是效率低下,因为 synchronized 是互斥锁,后续所有 getHelper 调用都得加锁。于是我们希望在 helper 正确初始化后就不再加锁了,尝试如下实现:

class Foo {private Helper helper = null;public synchronized Helper getHelper() {if (helper == null)             // ① 第一次检查synchronized(this) {        // ② 对 helper 加锁if (helper == null)         // ③ 同上个实现helper = new Helper();}return helper;}// other functions and members...
}

代码的初衷是:

  1. 如果正确初始化后,所有的 getHelper ① 的条件失败,于是不需要synchronized
  2. 如果未被正确初始化,则同上个实现一样,加锁进行初始化。

Unfortunately, that code just does not work in the presence of either optimizing compilers or shared memory multiprocessors.

很可惜,这段代码在编译器优化或多核的环境下是“错误”的。在这章中,我们会尝试去理解为什么它不正确,及为什么一些 bugfix 后依旧不正确。丑话说在前:

There is no way to make it work without requiring each thread that accesses the helper object to perform synchronization.

用人话来说,就是如果不把 helper 对象设置成 volatile 的,这段代码就不可能正确。

指令重排

第一个可能的问题是重排序1。这行代码 helper = new Helper(); 看上去是原子,从字节码的角度可以理解成下面几个步骤:

instance = Helper.class.newInstance(); // 1. 分配内存
Helper::constructor(instance);         // 2. 调用构造函数初始化对象
helper = instance;                     // 3. 让 helper 指向新的对象

前面章节说过,JVM 可能会对指令做重排序,所做的保证是不影响“单线程”的执行结果,那么可能排序成这样:

instance = Helper.class.newInstance(); // 1. 分配内存
helper = instance;                     // 3. 让 helper 指向新的对象
Helper::constructor(instance);         // 3. 调用构造函数初始化对象

那么在 #3 执行之前,helper 指向的内存地址未被初始化,是不安全的。在多线程下,可能会变成:

--------------- Thread A -------------------+--------------- Thread B --------------
if (helper == null)                         |synchronized(this) {                      |if (helper == null) {                   |instance = Helper.class.newInstance();|helper = instance;                    || if (helper == null) // false| return helper| // ... do something with helper.Helper::constructor(instance);        |}                                       |}                                         |
return helper;                              |

即由于重排,helper 指针已经有值了,但是还未初始化,导致此时线程 B 拿着未初始化的 helper 做了其它的操作,这是有风险的。

注意的是,即使编译器不做重排序,CPU 和缓存也可能会做重排序。

试图挽救重排序

上面的问题,我们根本目标是要保证 synchronized 块结束时(初始化完成后),相应的值才被其它线程看到,于是我们可以用下面这个 trick:

class Foo {private Helper helper = null;public Helper getHelper() {if (helper == null) {Helper h;                     // ① 创建了临时变量synchronized(this) {h = helper;                 // ② 保证读取最新的 helper 值if (h == null)synchronized (this) {   // ③ 尝试用内部锁解决重排序h = new Helper();     // ④ 创建新的实例}                       // ⑤ 释放了内部的锁helper = h;                 // ⑥ 将新的实例赋值给 helper}}return helper;}// other functions and members...
}

这里的想法是想通过 ③ 处的锁来阻止重排序,更准确地说,是希望在 ⑤ 释放锁的地方能提供内存屏障(memory barrier),从而保证 h = new Helper 一定在 helper = h 之前执行。

很可惜这个“希望”现实中不成立。Happens Before 里规定的是:

监视器上的 unlock 操作 Happens Before 同一个监视器的 lock 操作

换言之,为了保证 unlock Happens Before 其它的 lock 操作,JVM 需要保证在锁释放时,synchronized 块之前的操作都已经完成并写回到内存里。但是这个规则并没有说 synchronized 块之后的操作不能重排序到synchronized 块之前执行。因此上面这种修改的“美好希望”实际上并不成立2。

此路不通

即使我们真的能保证 helper 在被赋值之前就已经正确初始化了3,这种方式就能正确工作了吗?不能。

问题不仅仅在于写的一方,即使 helper 被正确初始化并赋值,由于另一个线程所在的 CPU 可能会从缓存中读取 helper 的值,如果 helper 的新值还没有被更新到缓存中,则读取的值可能还是 null

等等!不是说 synchronized 会保证可见性吗?是的,但它保证的是 unlock 操作前的更新对同一个监视器的 lock 操作可见,但现在另一个线程根本没有进入 synchronized 代码块,此时 JVM 不保证可见。

volatile

经过前面的分析,想起了前面章节提到的 volatile 关键字(JDK 1.5 后支持)有这么一条 Happens Before 规则:

volatile 变量规则:写入 volatile 变量 Happens Before 读取该变量

它可以提供额外的可见性保证。于是我们可以这么(正确)实现:

class Foo {private volatile Helper helper = null; // 注意变量声明了 volatilepublic Helper getHelper() {if (helper == null) {synchronized(this) {if (helper == null)helper = new Helper();}}return helper;}
}

这个实现里,写入 helper 之前的操作,如 Helper 对象的初始化,在 helper 被读取(如判断 helper == null)必须可见。换句话说,前文讨论的两种情况:重排序与可见性问题都由于 volatile 的语义得到保证。

那么 volatile 是不是会降低性能?《Java 并发编程实战》第三章的注解里说

在当前大多数处理器架构上,读取 volatile 变量的开销只比读取非 volatile 变量的开销略高一点

几个例外

例外不是说 volatile 方式的正确性有例外,而是对于一些特殊情形,有特殊的解法。

static 单例

对于是 static 的单例,最好的初始化方式是利用 Java 类加载机制,如下:

 
public class Foo {private static class Holder {private static Helper helper = new Helper();}public static Helper getInstance() {return Holder.helper;}
}

32 位 primitive

这里的知识点是 32 位的 primitive 类型变量的读写是原子的。如果初始化的方法是幂等的,则可以这么实现:

 
class Foo {private int cachedHashCode = 0;public int hashCode() {int h = cachedHashCode;if (h == 0)synchronized(this) {if (cachedHashCode != 0) return cachedHashCode;h = computeHashCode();cachedHashCode = h;}return h;}// other functions and members...
}

当然,如果方法是幂等的,甚至都不需要同步:

 
class Foo {private int cachedHashCode = 0;public int hashCode() {int h = cachedHashCode;if (h == 0) {h = computeHashCode();cachedHashCode = h;}return h;}// other functions and members...
}

为什么一定需要 32 位呢?因为 64 位的操作不是原子的,于是可能造成前后 32 位不是一起写入内存的,而另一个线程只读取先写入的 32 位,读到的结果不正确。

final

如果前文的 Helper 类是不可变的(immutable),具体地说,Helper 的所有属性都是 final 的,那么即使不加 volatile,DCL 也是正确的。这是因为 JVM 对 final 关键字有一些特殊的语义,有兴趣的可以参考 JSL 第 17 章

小结

本章中我们讲解了 The "Double-Checked Locking is Broken" Declaration 文章中关于 DCL 的各个示例,并结合前面章节中学到的 Happens Before 关系的知识去理解 DCL 成立或不成立的原因。

有时候我们会认为:写的时候加锁就行了,读操作不需要加锁。本节的例子就说明了这种观点不成立,会有可见性和顺序性的问题。最简单的解决方式是读操作也加锁,如果性能达不到要求,也可以像本节一样使用 volatile,但我个人不建议这么用,因为有太多细节需要考虑,可以使用 JUC 中的 ReadWriteLock 来加读写锁。

可以看到,要正确地实现并发程序,难度是很大的,并且要了解很多细节。当然也不必灰心,已经有前人为我们辅好了路,日常工作中我们只需要跟随前人的脚步,就可以满足绝大多数需求。

相关文章:

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

创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家! 双重锁定检查(Double Checked Locking,下称 DCL)是并发下实现懒…...

Redis(非关系型数据库)的作用 详细解读

edis(Remote Dictionary Server)是一个开源的、高性能的、基于内存的数据结构存储系统。它具有极高的读写性能,并且能够支持多种数据结构的存储。Redis 最初的设计目标是作为一个缓存解决方案,但随着其功能的不断扩展,…...

互联网视频推拉流EasyDSS视频直播点播平台视频转码有哪些技术特点和应用?

视频转码本质上是一个先解码再编码的过程。在转码过程中,原始视频码流首先被解码成原始图像数据,然后再根据目标编码标准、分辨率、帧率、码率等参数重新进行编码。这样,转换前后的码流可能遵循相同的视频编码标准,也可能不遵循。…...

python之多元线性回归

目录 前言实战 前言 多元线性回归是回归分析中的一种复杂模型,它考虑了多个输入变量对输出变量的影响。与一元线性回归不同,多元线性回归通过引入多个因素,更全面地建模了系统关系。 多元线性回归模型的表达式为: f ( X ) K T …...

学习threejs,使用设置lightMap光照贴图创建阴影效果

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️THREE.MeshLambertMaterial…...

一,SQL注入解题(猫舍)

封神台 第一章:为了女神小芳! Tips: 通过sql注入拿到管理员密码! 尤里正在追女神小芳,在得知小芳开了一家公司后,尤里通过whois查询发现了小芳公司网站 学过一点黑客技术的他,想在女神面前炫炫技。于是他…...

海康大华宇视视频平台EasyCVR私有化部署视频平台海康ISUP是什么?如何接入到EasyCVR?

在现代安防领域,随着技术的发展和需求的增加,对于视频监控系统的远程管理和互联互通能力提出了更高的要求。海康威视的ISUP协议(以及功能相似的EHOME协议)因此应运而生,它们为不具备固定IP接入的设备提供了一种有效的中…...

Java ArrayList 与顺序表:在编程海洋中把握数据结构的关键之锚

我的个人主页 我的专栏:Java-数据结构,希望能帮助到大家!!!点赞❤ 收藏❤ 前言:在 Java编程的广袤世界里,数据结构犹如精巧的建筑蓝图,决定着程序在数据处理与存储时的效率、灵活性以…...

windows下安装wsl的ubuntu,同时配置深度学习环境

写在前面,本次文章只是个人学习记录,不具备教程的作用。个别信息是网上的,我会标注,个人是gpt生成的 安装wsl 直接看这个就行;可以不用备份软件源。 https://blog.csdn.net/weixin_44301630/article/details/1223900…...

开展网络安全成熟度评估:业务分析师的工具和技术

想象一下,您坐在飞机驾驶舱内。起飞前,您需要确保所有系统(从发动机到导航工具)均正常运行。现在,将您的业务视为飞机,将网络安全视为飞行前必须检查的系统。就像飞行员依赖检查表一样,业务分析师使用网络安全成熟度评估来评估组织对网络威胁的准备程度。这些评估可帮助…...

Maven Surefire 插件简介

Maven Surefire 插件是 Maven 构建系统中的一个关键组件,专门用于在构建生命周期中执行单元测试。 它通常与 Maven 构建生命周期的测试阶段绑定,确保所有单元测试在项目编译后和打包前被执行。 最新版本 Maven Surefire 插件的最新版本为 3.5.2。 使…...

基于微信小程序的平价药房管理系统+LW参考示例

1.项目介绍 系统角色:管理员、医生、普通用户功能模块:用户管理、医生管理、药品分类管理、药品信息管理、在线问诊管理、生活常识管理、日常提醒管理、过期处理、订单管理等技术选型:SpringBoot,Vue,uniapp等测试环境…...

react 前端最后阶段静态服务器启动命令

这个错误是因为你还没有安装 serve 工具。让我们一步步解决: 首先全局安装 serve: npm install -g serve如果上面的命令报错,可能是因为权限问题,可以尝试: 安装完成后,再运行: Windows 下使用…...

Flink中普通API的使用

本篇文章从Source、Transformation(转换因子)、sink这三个地方进行讲解 Source: 创建DataStream本地文件SocketKafka Transformation(转换因子): mapFlatMapFilterKeyByReduceUnion和connectSide Outpu…...

高性能 ArkUI 应用开发:复杂 UI 场景中的内存管理与 XML 优化

本文旨在深入探讨华为鸿蒙HarmonyOS Next系统(截止目前API12)的技术细节,基于实际开发实践进行总结。 主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。 本文为原创内容,任何形式的转载必须注明出处及原作者。 在开发高性能 ArkUI 应…...

用天翼云搭建一个HivisionIDPhoto证件照处理网站

世人不必记我,我不记世人。 HivisionIDPhoto证件照处理网站 世人不必记我,我不记世人。项目地址项目搭建与修改前端后端遇到的坑 成果图 前段时间工作需要频繁处理证件照,当时同事推荐一个证件照小程序(要看广告)&…...

【算法一周目】滑动窗口(2)

目录 水果成篮 解题思路 代码实现 找到字符串中所有字母异位词 解题思路 代码实现 串联所有单词的子串 解题思路 代码实现 最小覆盖子串 解题思路 代码实现 水果成篮 题目链接:904. 水果成篮 题目描述: 你正在探访一家农场,农场…...

Zustand:一个轻量级的React状态管理库

文章目录 前言一、安装Zustand二、使用Zustand三、实际案例结语 前言 在现代Web开发中,状态管理是一个常见的需求,特别是在构建大型或复杂的单页面应用程序(SPA)时。React等框架虽然提供了基本的状态管理功能,但对于复…...

C++练级计划->《单例模式》懒汉和饿汉

目录 单例模式是什么? 单例模式的应用: 饿汉单例模式: 1.实现: 2.理解: 懒汉单例模式: 1.实现: 2.理解: 懒汉和饿汉的优缺点 饿汉模式的优点: 饿汉模式的缺点&a…...

SQL for XML

关系数据模型与SQL SQL for XML 模式名功能RAW返回的行作为元素,列值作为元素的属性AUTO返回表名对应节点名称的元素,每列的属性作为元素的属性输出输出,可形成简单嵌套结构EXPLICIT通过SELECT语法定义输出XML结构PATH列名或列别名作为XPAT…...

如何使用GCC手动编译stm32程序

如何不使用任何IDE(集成开发环境)编译stm32程序? 集成开发环境将编辑器、编译器、链接器、调试器等开发工具集成在一个统一的软件中,使得开发人员可以更加简单、高效地完成软件开发过程。如果我们不使用KEIL,IAR等集成开发环境,…...

在线绘制Nature Communication同款双色、四色火山图,突出感兴趣的基因

导读:火山图通常使用三种颜色分别表示显著上调,显著下调和不显著。通过为特定的数据点添加另一种颜色,可以创建双色或四色火山图,从而更直观地突出感兴趣的数据点。 《Nature Communication》文章“Molecular and functional land…...

C语言:C语言实现对MySQL数据库表增删改查功能

基础DOME可以用于学习借鉴&#xff1b; 具体代码 #include <stdio.h> #include <mysql.h> // mysql 文件&#xff0c;如果配置ok就可以直接包含这个文件//宏定义 连接MySQL必要参数 #define SERVER "localhost" //或 127.0.0.1 #define USER "roo…...

C++ 二叉搜索树(Binary Search Tree, BST)深度解析与全面指南:从基础概念到高级应用、算法优化及实战案例

&#x1f31f;个人主页&#xff1a;落叶 &#x1f31f;当前专栏: C专栏 目录 ⼆叉搜索树的概念 ⼆叉搜索树的性能分析 ⼆叉搜索树的插⼊ ⼆叉搜索树的查找 二叉搜索树中序遍历 ⼆叉搜索树的删除 cur的左节点为空的情况 cur的右节点为空的情况 左&#xff0c;右节点都不为…...

刷题日常(移动零,盛最多水的容器,三数之和,无重复字符的最长子串)

移动零 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 俩种情况&#xff1a; 1.当nums[i]为0的时候 直接i 2.当nums[i]不为0的时候 此时 …...

深入了解决策树---机器学习中的经典算法

引言 决策树&#xff08;Decision Tree&#xff09;是一种重要的机器学习模型&#xff0c;以直观的分层决策方式和简单高效的特点成为分类和回归任务中广泛应用的工具。作为解释性和透明性强的算法&#xff0c;决策树不仅适用于小规模数据&#xff0c;也可作为复杂模型的基石&…...

Elasticsearch对于大数据量(上亿量级)的聚合如何实现?

大家好&#xff0c;我是锋哥。今天分享关于【Elasticsearch对于大数据量&#xff08;上亿量级&#xff09;的聚合如何实现&#xff1f;】面试题。希望对大家有帮助&#xff1b; Elasticsearch对于大数据量&#xff08;上亿量级&#xff09;的聚合如何实现&#xff1f; 1000道 …...

深度学习模型:循环神经网络(RNN)

一、引言 在深度学习的浩瀚海洋里&#xff0c;循环神经网络&#xff08;RNN&#xff09;宛如一颗独特的明珠&#xff0c;专门用于剖析序列数据&#xff0c;如文本、语音、时间序列等。无论是预测股票走势&#xff0c;还是理解自然语言&#xff0c;RNN 都发挥着举足轻重的作用。…...

前端---HTML(一)

HTML_网络的三大基石和html普通文本标签 1.我们要访问网络&#xff0c;需不需要知道&#xff0c;网络上的东西在哪&#xff1f; 为什么我们写&#xff0c;www.baidu.com就能找到百度了呢&#xff1f; 我一拼ping www.baidu.com 就拼到了ip地址&#xff1a; [119.75.218.70]…...

SQL 复杂查询

目录 复杂查询 一、目的和要求 二、实验内容 &#xff08;1&#xff09;查询出所有水果产品的类别及详情。 查询出编号为“00000001”的消费者用户的姓名及其所下订单。&#xff08;分别采用子查询和连接方式实现&#xff09; 查询出每个订单的消费者姓名及联系方式。 在…...

免费下载网站模板/青岛网络优化厂家

1.虚拟IP 在 TCP/IP 的架构下&#xff0c;所有想上网的电脑&#xff0c;不论是用何种方式连上网路&#xff0c;都必须要有一个唯一的 IP-address。事实上IP地址是主机硬件地址的一种抽象&#xff0c;简单的说&#xff0c;MAC地址是物理地址&#xff0c;IP地址是逻辑地址。 虚拟…...

涂料做哪个网站好/全网推广网站

三个月前刚毕业的时候&#xff0c;听到存储过程就头疼。写一个SQL存储过程&#xff0c;建立一个表USER 字段是姓名&#xff0c;年龄&#xff0c;职位&#xff0c;权限&#xff0c;然后向里面插入6条数据&#xff0c;然后查询出年龄大于18的所有信息。下面是答案&#xff1a;复制…...

网站建设的主要工作有哪些/营销模式有哪些

java基础-不用ide如何打包 1. 建立目录 src存放源文件 classes存放编译文件 2. 建立类文件 主类 package test.ant; import test.ant.MyTools; // import com.alibaba.fastjson.JSONObject; public class HelloWorld {public static void main(String[] args) {System.out.prin…...

网站建设标准/免费信息推广平台

引用js和css很类似&#xff0c;大致有三种方式&#xff1a; 第一种&#xff1a; 在行内引用js&#xff0c; <div onclick"alert(111);"></div> 第二种&#xff1a; 在行外引用js&#xff0c; <script type"text/javascript">alert(2222)…...

佛山专业网站制作/2021关键词搜索排行

dialog使用的十分广泛&#xff0c;今天就介绍下包含了listview的dialog简单应用&#xff0c;其实和普通的dialog一样。1、先布局首先主布局android:id"id/btn_dialog_2"android:layout_width"match_parent"android:layout_height"wrap_content"a…...

景安网站备案要多久/湖南专业的关键词优化

2016-05-31 回答实现两个mysql数据库之间同步同步原理&#xff1a;mysql 为了实现replication 必须打开bin-log 项&#xff0c;也是打开二进制的mysql 日志记录选项。mysql 的bin log 二进制日志&#xff0c;可以记录所有影响到数据库表中存储记录内容的sql 操作&#xff0c;如…...