常见线程安全问题之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...
}
代码的初衷是:
- 如果正确初始化后,所有的
getHelper
① 的条件失败,于是不需要synchronized
- 如果未被正确初始化,则同上个实现一样,加锁进行初始化。
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…...

XCTF-web-easyupload
试了试php,php7,pht,phtml等,都没有用 尝试.user.ini 抓包修改将.user.ini修改为jpg图片 在上传一个123.jpg 用蚁剑连接,得到flag...

遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
Neo4j 集群管理:原理、技术与最佳实践深度解析
Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...