常见线程安全问题之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…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
椭圆曲线密码学(ECC)
一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...

视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...

C# 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

tauri项目,如何在rust端读取电脑环境变量
如果想在前端通过调用来获取环境变量的值,可以通过标准的依赖: std::env::var(name).ok() 想在前端通过调用来获取,可以写一个command函数: #[tauri::command] pub fn get_env_var(name: String) -> Result<String, Stri…...

WebRTC调研
WebRTC是什么,为什么,如何使用 WebRTC有什么优势 WebRTC Architecture Amazon KVS WebRTC 其它厂商WebRTC 海康门禁WebRTC 海康门禁其他界面整理 威视通WebRTC 局域网 Google浏览器 Microsoft Edge 公网 RTSP RTMP NVR ONVIF SIP SRT WebRTC协…...