【JUC】深入解析 JUC 并发编程:单例模式、懒汉模式、饿汉模式、及懒汉模式线程安全问题解析和使用 volatile 解决内存可见性问题与指令重排序问题
单例模式
单例模式确保某个类在程序中只有一个实例,避免多次创建实例(禁止多次使用new
)。
要实现这一点,关键在于将类的所有构造方法声明为private
。
这样,在类外部无法直接访问构造方法,new
操作会在编译时报错,从而保证类的实例唯一性。例如,在JDBC中,DataSource
实例通常只需要一个,单例模式非常适合这种场景。
单例模式的实现方式主要有两种:“饿汉式”和“懒汉式”
饿汉模式
下面这段代码,是对唯一成员 instance 进行初始化,用 static 修饰 instance,对 instance 的初始化,会在类加载的阶段触发;类加载往往就是在程序一启动就会触发;
由于是在类加载的阶段,就早早地创建好了实例(static修饰),这也就是“饿汉模式” 名字的由来。
在初始化好 instance 后,后续统一通过调用 getInstance() 方法获取 instance
单例模式的“点睛之笔”,用 private 修饰类中所有构造方法
,因为可以防止通过 new 关键字在类外部创建实例
,只能通过调用内部静态方法,来获取单例类实例:
懒汉模式
- 饿汉模式:
在类加载时即创建实例
,通过将构造方法声明为private
,防止外部创建其他实例。- 懒汉模式:延迟创建
实例,仅在真正需要时才创建。
这种模式在某些情况下无需实例对象时,可避免不必要的实例化,减少开销并提升效率。
单线程版本
在懒汉模式下,实例的创建时机是在第一次被使用时
,而不是在程序启动时。
如果程序启动后立即需要使用实例,那么懒汉模式和饿汉模式的效果相似。
然而,如果程序运行了较长时间仍未使用该实例,懒汉模式会延迟实例的创建,从而减少不必要的开销
。
多线程版本
单例模式产生线程安全的原因
饿汉模式
懒汉模式
为什么会有单线程版本和多线程版本的懒汉模式写法呢?我们来看单线程版本,如果运用到多线程的环境下,会出现什么问题:
在懒汉模式中,instance
被声明为static
,因此多个线程调用getInstance()
时,返回的是同一个实例。
然而,getInstance()
方法中既包含读操作(检查instance
是否为null
),也包含写操作(实例化instance
)。
尽管赋值操作本身是原子的,但整个getInstance()
方法并非原子操作。这意味着在多线程环境下,判断和赋值操作不能保证紧密执行,从而导致线程安全问题。
在多线程环境下,若两个线程(如 t1 和 t2)同时执行 getInstance()
方法,可能会导致值覆盖问题。
如上图,t2 线程的赋值操作可能会覆盖 t1 线程新创建的对象,导致第一个线程创建的对象被垃圾回收(GC)
。
这不仅增加了不必要的开销,还违背了单例模式的核心目标:避免重复创建实例,减少耗时操作,节省资源。
即使第一个对象很快被释放,其创建过程中的数据加载依然会产生额外开销。
总结:
- 饿汉模式:仅涉及对实例的读操作,不涉及写操作,因此天然线程安全。无论在单线程还是多线程环境下,其基本形式保持不变。
- 懒汉模式:在
getInstance()
中包含紧密相关的读写操作(检查实例是否存在并创建实例),但这些操作无法紧密执行,导致线程安全问题。
解决单例模式的线程安全问题
面试题:
这两个单例模式的 getInstance() 在多线程环境下调用,是否会出现 bug,如何解决 bug?
1. 通过加锁让读写操作紧密执行
饿汉模式本身不存在线程安全问题,因为它仅涉及读操作,不涉及写操作。
然而,懒汉模式在多线程环境下可能出现线程安全问题,原因在于getInstance()
方法中的读写操作(判断 + 赋值)不能紧密执行。
为解决这一问题,需要对相关操作进行加锁,以确保线程安全。
方法一:对方法中的读操作加锁
这样加锁后,如果 t1 和 t2 还出现下图读写逻辑的执行顺序:
- t2 会阻塞等待 t1(或 t1 等待 t2)完成对象的创建(读写操作结束后),释放锁后,第二个线程才能继续执行。
- 此时,第二个线程发现
instance
已非null
,会直接返回已创建的实例,不再重复创建。
方法二:对整个方法加锁
直接对getInstance()
方法加锁,也能确保读写操作紧密执行。此时,锁对象为SingletonLazy.class
。这两种方法的效果相同
2. 处理加锁引入的新问题
问题描述
对于当前懒汉模式的代码,多个线程共享一把锁,不会导致死锁
。只需确保第一个线程调用getInstance()
时,读写操作紧密执行即可。
后续线程在读取时发现instance != null
,就不会触发写操作
,从而自然保证了线程安全。
然而,若每次调用getInstance()
方法时都进行加锁解锁操作,由于synchronized
是重量级锁,多次加锁,尤其是重量级锁会导致显著的性能开销,从而降低程序效率
。
拓展:
StringBuffer 就是为了解决,大量拼接字符串时,产生很多中间对象问题而提供的一个类,提供 append 和 insert 方法,可以将字符串添加到,已有序列的 末尾 或 指定位置。
StringBuffer 的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上了synchronized。但是保证了线程安全是需要性能的代价的。
在很多情况下我们的字符串拼接操作,不需要线程安全,这时候 StringBuilder 登场了,
StringBuilder 是 JDK1.5 发布的, StringBuilder 和 StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。所以在单线程情况下,优先考虑使用 StringBuilder。
StringBuffer 和 StringBuilder 二者都继承了 AbstractStringBuilder,底层都是利用可修改的 char数组 (JDK9以后是 byte 数组)。
所以如果我们有大量的字符串拼接,如果能预知大小的话最好在new StringBuffer 或者 new StringBuilder 的时候设置好 capacity ,避免多次扩容的开销(扩容要抛弃原有数组,还要进行数组拷贝创建新的数组)。
解决方法
再嵌套一次判断操作,既可以保证线程安全,又可以避免大量加锁解锁产生的开销:
在单线程环境下,嵌套两层相同的if
语句并无意义,因为单线程只有一个执行流,嵌套与否结果相同。但在多线程环境下,多个并发执行流,可能导致不同线程在执行判断操作时,因其他线程修改了instance
而得到不同结果。
例如,在懒汉模式下,即使两个
if
语句形式相同
,其目的和作用却不同
:
- 第一个
if
用于判断是否需要加锁;- 第二个
if
用于判断是否需要创建对象。这种结构虽看似巧合,但实则必要。
3. 引入 volatile 关键字
问题描述
在懒汉模式的单例实现中
,使用volatile
关键字修饰instance
至关重要。以下是懒汉模式
的单例实现代码:
private static SingletonLazy instance = null;public static SingletonLazy getInstance() {if (instance == null) {synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}}}return instance;
}
如果不使用volatile
修饰instance
,可能会出现以下问题:
内存可见性问题
核心问题
- 在没有
volatile
修饰时,线程t1
对instance
的写入可能仅停留在线程本地缓存(CPU缓存或寄存器),而非立即同步到主内存
。 - 此时线程
t2
读取的可能是自己缓存中的旧值(null
),即使t1
已完成初始化。 - 即使
t2
进入同步块,第一次判空(if (instance == null)
仍可能读取到未更新的缓存值,导致不必要的锁竞争。 - 第二次判空
if (instance == null)
,t2
可能会错误地认为instance == null
,并再次执行实例化逻辑,导致又重复创建了新的实例。
内存可见性底层分析
- 硬件层面的原因
存储层级 | 读写速度 | 存储大小 | 特性 |
---|---|---|---|
寄存器 | 最快 | 最小(几十字节) | CPU直接计算使用的临时存储 |
CPU缓存 (L1/L2/L3) | 快 | 较小(KB~MB级) | 每个CPU核心/多核共享,减少访问主存延迟 |
主内存 (RAM) | 慢 | 大(GB级) | 所有线程共享,但访问速度比缓存慢100倍以上 |
- 速度差异:CPU为了避免等待慢速的主内存读写,会优先使用缓存和寄存器(如将
instance
的值缓存在核心的L1缓存中)。 - 副作用:线程
t1
修改instance
后,可能仅更新了当前核心的缓存,而其他核心的缓存或主内存未被同步,导致t2
读取到过期数据。
- Java内存模型(JMM)的抽象
- 硬件差异被
JMM
抽象为工作内存(线程私有)
和主内存(共享)
的分离: 工作内存
:包含CPU寄存器、缓存等线程私有的临时存储
。主内存
:所有线程共享的真实内存
。
- 问题本质:
- 当线程
t1
未强制同步(如缺少volatile
或锁)时,JVM/CPU
可能延迟将工作内存的修改刷回主内存,其他线程也无法感知变更。
指令重排序
指令重排序的具体问题
instance = new SingletonLazy()
的实际操作可分为以下步骤(可能被JVM/CPU重排序):
1. 分配对象内存空间(堆上分配,此时内存内容为默认值0/null)
2. 调用构造函数(初始化对象字段)
3. 将引用赋值给 instance 变量(此时 instance != null)
可能的危险重排序:
- JVM可能将步骤 3(赋值) 和 2(构造) 调换顺序,导致:
1. 分配内存
2. 赋值给 instance(此时 instance != null,但对象未初始化!)
3. 执行构造函数
这就是指令重排序问题。
- 多线程场景下指令重排序的后果
- 线程 t1 执行
getInstance()
时发生重排序:- 先执行步骤1和3,
instance
已不为null
,但对象未构造完成。
- 先执行步骤1和3,
- 线程 t2 调用
getInstance()
:- 第一次判空
if (instance == null)
会跳过 - 若 t2 立刻调用
instance.func()
,会访问未初始化的字段,导致:- 空指针异常(如果
func()
访问未初始化的引用字段)。 - 数据不一致(如果
func()
依赖构造函数中初始化的值)。
- 空指针异常(如果
- 第一次判空
解决方法
使用volatile
修饰instance
后,不仅能确保每次读取操作都直接从内存中读取,还能防止与该变量相关的读取和修改操作发生重排序。
private volatile static SingletonLazy instance;public static SingletonLazy getInstance() {if (instance == null) { // 第一次无锁检查synchronized (locker) { // 同步块if (instance == null) { // 第二次检查instance = new SingletonLazy(); // 受volatile保护}}}return instance;
}
volatile
是怎么解决内存可见性
问题的呢?
通过内存屏障(Memory Barrier)
直接操作硬件层
:
- 写操作:强制将当前核心的缓存行(Cache Line)写回主内存,并失效其他核心的缓存。
- 读操作:强制从主内存重新加载数据,跳过缓存。
private static volatile SingletonLazy instance; // 通过volatile禁止缓存优化
总结
- 直接原因:CPU缓存和寄存器的速度优化导致可见性问题。
- 根本原因:硬件架构与编程语言内存模型的设计差异(JMM需在性能与正确性间权衡)。
- 解决方案:
volatile
通过内存屏障强制同步硬件层和JMM的约定。
总结:为什么双重检查锁(DCL)必须用volatile
?
- 可见性:确保
t1
的初始化结果对t2
立即可见。 - 禁止指令重排序:
instance = new SingletonLazy()
的字节码可能被重排序为:- 分配内存空间
- 将引用写入
instance
(此时instance != null
但对象未初始化!) - 执行构造函数
volatile
会禁止这种重排序,保证步骤2在3之后执行
。
4. 指令重排序问题
模拟编译器指令重排序情景
要在超市中买到左边购物清单的物品,有两种买法
方法一:根据购物清单的顺序买;(按照程序员编写的代码顺序进行编译)
方法二:根据物品最近距离购买;(通过指令重排序后再编译)
两种方法都能买到购物清单的所有物品,但是比起第一种方法,第二种方法在不改变原有逻辑的情况下,优化执行指令顺序,更高效地执行完所有的指令
。
指令重排序概述
指令重排序的定义
指令重排序是指编译器或处理器为了提高性能,在不改变程序执行结果的前提下,对指令序列进行重新排序的优化技术。这种技术可以让计算机在执行指令时更高效地利用计算资源,从而提高程序的执行效率。
指令重排序的类型
- 编译器重排序
编译器在生成目标代码时会对源代码中的指令进行优化和重排,以提高程序的执行效率。这一过程在编译阶段完成,目的是生成更高效的机器代码。
- 处理器重排序
处理器在执行指令时也可以对指令进行重排序,以最大程度地利用处理器的流水线和多核等特性,从而提高指令的执行效率。
指令重排序引发的问题
尽管指令重排序可以提高程序的执行效率,但在多线程编程中可能会引发内存可见性问题。由于指令重排序可能导致共享变量的读写顺序与代码中的顺序不一致,当多个线程同时访问共享变量时,可能会出现数据不一致的情况。
指令重排序解决方案
为了解决指令重排序带来的问题,可以采取以下措施:
- 编译器层面:通过禁止特定类型的编译器重排序,确保指令的执行顺序符合预期。
- 处理器层面:通过插入
内存屏障(Memory Barrier)
来禁止特定类型的处理器重排序。内存屏障是一种CPU指令,用来禁止处理器指令发生重排序,从而保障指令执行的有序性。此外,内存屏障还会在处理器写入或读取值之前,将主内存的值写入高速缓存并清空无效队列,从而保障变量的可见性。
相关文章:

【JUC】深入解析 JUC 并发编程:单例模式、懒汉模式、饿汉模式、及懒汉模式线程安全问题解析和使用 volatile 解决内存可见性问题与指令重排序问题
单例模式 单例模式确保某个类在程序中只有一个实例,避免多次创建实例(禁止多次使用new)。 要实现这一点,关键在于将类的所有构造方法声明为private。 这样,在类外部无法直接访问构造方法,new操作会在编译…...

2025年全国青少年信息素养大赛复赛C++算法创意实践挑战赛真题模拟强化训练(试卷3:共计6题带解析)
2025年全国青少年信息素养大赛复赛C++算法创意实践挑战赛真题模拟强化训练(试卷3:共计6题带解析) 第1题:四位数密码 【题目描述】 情报员使用4位数字来传递信息,同时为了防止信息泄露,需要将数字进行加密。数据加密的规则是: 每个数字都进行如下处理:该数字加上5之后除…...

Mongodb | 基于Springboot开发综合社交网络应用的项目案例(中英)
目录 Project background Development time Project questions Create Project create springboot project project framework create folder Create Models user post Comment Like Message Serive tier user login and register Dynamic Publishing and Bro…...

飞腾D2000与FPGA结合的主板
UD VPX-404是基于高速模拟/数字采集回放、FPGA信号实时处理、CPU主控、高速SSD实时存储架构开发的一款高度集成的信号处理组合模块,采用6U VPX架构,模块装上外壳即为独立整机,方便用户二次开发。 UD VPX-404模块的国产率可达到100%࿰…...

百度量子蜘蛛3.0横空出世,搜索引擎迎来“量子跃迁“级革命
一、量子蜘蛛3.0的三大颠覆性升级 1. 动态抓取:让内容实时"量子纠缠" - 智能频率调节:根据网站更新频率自动调整抓取节奏,新闻类站点日抓取量达3-5次,静态页面抓取间隔延长至72小时。某财经媒体通过"热点事件15分钟…...

GitHub开源|AI顶会论文中文翻译PDF合集(gpt-translated-pdf-zh)
项目核心特点 该项目专注于提供计算机科学与人工智能领域的高质量中文翻译资源,以下为关键特性: 主题覆盖广泛:包含算法、数据结构、概率统计等基础内容,以及深度学习、强化学习等前沿研究方向。格式统一便捷:所有文…...
JSR 303(即 Bean Validation)是一个通过注解在 Java Bean 上定义和执行验证规则的规范
🛠️ 一、JSR 303是什么? JSR 303(Java Specification Requests 303)是Java EE 6的子规范,全称Bean Validation。它通过注解方式对JavaBean的属性值进行标准化校验,例如检查非空、长度、格式等规则…...
5G 网络中的双向认证机制解析
一、5G 网络中的双向认证机制解析 在 5G 核心网中,双向认证是指UE(用户设备)与网络互相验证对方身份的过程。这一机制通过多层次的安全协议和密钥交换,确保通信双方的合法性,防止中间人攻击和身份伪造。 1. UE 存储的关键信息 UE 作为用户终端,存储以下核心安全信息:…...
DAY07:Vue Router深度解析与多页面博客系统实战
第一部分:Vue Router核心概念深度剖析 1.1 现代前端路由的本质 在单页应用(SPA)时代,前端路由扮演着至关重要的角色。它突破了传统多页面应用的跳转方式,通过以下机制实现无刷新页面切换: Hash模式&#…...

Drawio编辑器二次开发
Drawio (现更名为 Diagrams.net )是一款完全免费的在线图表绘制工具,由 JGraph公司 开发。它支持创建多种类型的图表,包括流程图、组织结构图、UML图、网络拓扑图、思维导图等,适用于商务演示、软件设计等多种场景…...

1.测试过程之需求分析和测试计划
测试基础 流程 1.分析测试需求 2.编写测试计划 3.设计与编写测试用例 4.执行测试 5.评估与总结 测试目标 根据测试阶段不同可分为四个主要目标:预防错误(早期)、发现错误(开发阶段)、建立信心(验收阶段&a…...
第三十七天打卡
过拟合的判断:测试集和训练集同步打印指标模型的保存和加载 仅保存权重保存权重和模型保存全部信息checkpoint,还包含训练状态 早停策略 过拟合判断 import torch import torch.nn as nn import torch.optim as optim from sklearn.datasets import load…...
Qt 窗口标志(Window Flags)详解:控制窗口样式与行为
在 Qt 中,windowFlags 用于控制窗口的样式和行为,包括标题栏、边框、最大化/最小化按钮等。合理设置 windowFlags 可以自定义窗口的外观和交互方式。本文将详细介绍常用的窗口标志及其组合效果。 1. 基本概念 windowFlags 是一个 Qt::WindowFlags 类型的…...
ABP VNext + CRDT 打造实时协同编辑
🛠️ ABP VNext CRDT 打造实时协同编辑器 🎉 📚 目录 🛠️ ABP VNext CRDT 打造实时协同编辑器 🎉🧠 背景与挑战🔹 系统架构🛣️ 端到端流程 🚦🔒 安全与鉴…...
微信小程序真机调试时如何实现与本地开发环境服务器交互
最近在开发微信小程序项目,真机调试时需要在手机上运行小程序,为了实现本地开发服务器与手机小程序的交互,需要以下步骤 1.将手机连到和本地一样的局域网 2.Visual Studio中将IIS Express服务器的localhost端口地址修改为本机的IP自定义的端口: 1)找到web api项目…...
Linux: network: dpdk, VF, ip link set down 对VF不生效
文章目录 问题另一个测试的结果是从dpdk的文档看怎么设置VF给VM内核的调用需要使用的命令问题 最近遇到一个问题,也可以说是一种常识,至少是之前不知道的常识:如果一个VF分配给了VM用作dpdk的输入。在host做ip link set down 这个PF的接口,对这个VM里的VF的功能没有任何影…...

[春秋云镜] CVE-2023-23752 writeup
首先奉上大佬的wp表示尊敬:(很详细)[ 漏洞复现篇 ] Joomla未授权访问Rest API漏洞(CVE-2023-23752)_joomla未授权访问漏洞(cve-2023-23752)-CSDN博客 知识点 Joomla版本为4.0.0 到 4.2.7 存在未授权访问漏洞 Joomla是一套全球知名的内容管理…...
Java集合操作常见错误与最佳实践
错误69:搜索无关类型的对象 泛型方法的类型安全漏洞 在Java引入参数化类型前,集合元素只能声明为Object类型,导致可以随意将字符串添加到数值列表中。虽然泛型机制对添加元素的方法进行了类型约束,但搜索和删除相关方法仍保留了Object类型的参数设计。这包括以下关键方法…...

CSS专题之水平垂直居中
前言 石匠敲击石头的第 16 次 在日常开发中,经常会遇到水平垂直居中的布局,虽然现在基本上都用 Flex 可以轻松实现,但是在某些无法使用 Flex 的情况下,又应该如何让元素水平垂直居中呢?这也是一道面试的必考题…...
python打卡day41@浙大疏锦行
知识回顾 1. 数据增强 2. 卷积神经网络定义的写法 3. batch归一化:调整一个批次的分布,常用与图像数据 4. 特征图:只有卷积操作输出的才叫特征图 5. 调度器:直接修改基础学习率 卷积操作常见流程如下: 1. …...
vue3 基本语法 父子关系
在Vue 3中,父子组件的关系是通过组件的嵌套实现的。父组件可以传递数据(props)给子组件,同时子组件可以通过事件(emits)与父组件通信。下面是如何在Vue 3中建立和使用父子组件的基本语法: 1. 创…...
算法-js-子集
题:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 方法一:迭代法 核心逻辑:动态扩展子集, 小规…...

(新)MQ高级-MQ的可靠性
消息到达MQ以后,如果MQ不能及时保存,也会导致消息丢失,所以MQ的可靠性也非常重要。 一、数据持久化 为了提升性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失。为了保证数据的可靠性,必须…...
Android设置界面层级为最上层实现
Android设置界面层级为最上层实现 文章目录 Android设置界面层级为最上层实现一、前言二、Android设置界面层级为最上层实现1、主要代码2、后遗症 三、其他1、Android设置界面层级为最上层小结2、悬浮框的主要代码悬浮框 注意事项(1)权限限制(…...
云原生微服务架构演进之路:理念、挑战与实践
📝个人主页🌹:慌ZHANG-CSDN博客 🌹🌹期待您的关注 🌹🌹 一、引言:架构的演进是业务进化的技术反射 在软件行业的发展过程中,架构变迁总是伴随着技术浪潮与业务复杂度的升…...
Go语言使用阿里云模版短信服务
在当今的互联网项目中,短信验证码、通知等功能已成为标配。本文将详细介绍如何使用Go语言集成阿里云短信服务(DYSMSAPI)实现短信发送功能。 一、准备工作 在开始之前,您需要完成以下准备工作: 注册阿里云账号并实名认证开通短信服务(SMS)申…...

Leetcode 3231. 要删除的递增子序列的最小数量
1.题目基本信息 1.1.题目描述 给定一个整数数组 nums,你可以执行任意次下面的操作: 从数组删除一个 严格递增 的 子序列。 您的任务是找到使数组为 空 所需的 最小 操作数。 1.2.题目地址 https://leetcode.cn/problems/minimum-number-of-increas…...

4.2.5 Spark SQL 分区自动推断
在本节实战中,我们学习了Spark SQL的分区自动推断功能,这是一种提升查询性能的有效手段。通过创建具有不同分区的目录结构,并在这些目录中放置JSON文件,我们模拟了一个分区表的环境。使用Spark SQL读取这些数据时,Spar…...
基于昇腾MindSpeed训练加速库玩转智谱GLM-4-0414模型
智谱GLM-4-0414模型提供32B和9B两种参数规模,涵盖基础、推理和沉思等多种模型类型,均基于 MIT 许可协议开放。其中,推理模型 GLM-Z1-32B-0414 性能卓越,与 DeepSeek-R1 等领先模型相当,实测推理速度达每秒200个Tokens。…...

【图像处理入门】2. Python中OpenCV与Matplotlib的图像操作指南
一、环境准备 import cv2 import numpy as np import matplotlib.pyplot as plt# 配置中文字体显示(可选) plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False二、图像的基本操作 1. 图像读取、显示与保存 使用OpenCV…...