【JUC】深入解析 JUC 并发编程:单例模式、懒汉模式、饿汉模式、及懒汉模式线程安全问题解析和使用 volatile 解决内存可见性问题与指令重排序问题

article/2025/7/28 4:07:47

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


单例模式


单例模式确保某个类在程序中只有一个实例,避免多次创建实例(禁止多次使用new)。

要实现这一点,关键在于将类的所有构造方法声明为private

这样,在类外部无法直接访问构造方法,new操作会在编译时报错,从而保证类的实例唯一性。例如,在JDBC中,DataSource实例通常只需要一个,单例模式非常适合这种场景。

单例模式的实现方式主要有两种:“饿汉式”和“懒汉式”


饿汉模式


img


下面这段代码,是对唯一成员 instance 进行初始化,用 static 修饰 instance,对 instance 的初始化,会在类加载的阶段触发;类加载往往就是在程序一启动就会触发;img

由于是在类加载的阶段,就早早地创建好了实例(static修饰),这也就是“饿汉模式” 名字的由来。


在初始化好 instance 后,后续统一通过调用 getInstance() 方法获取 instance

img


单例模式的“点睛之笔”,用 private 修饰类中所有构造方法,因为可以防止通过 new 关键字在类外部创建实例,只能通过调用内部静态方法,来获取单例类实例:

img


img


懒汉模式


  • 饿汉模式在类加载时即创建实例,通过将构造方法声明为private,防止外部创建其他实例。
  • 懒汉模式:延迟创建实例,仅在真正需要时才创建。这种模式在某些情况下无需实例对象时,可避免不必要的实例化,减少开销并提升效率。

单线程版本


在懒汉模式下,实例的创建时机是在第一次被使用时,而不是在程序启动时。

如果程序启动后立即需要使用实例,那么懒汉模式和饿汉模式的效果相似。

然而,如果程序运行了较长时间仍未使用该实例,懒汉模式会延迟实例的创建,从而减少不必要的开销

img


多线程版本


img


单例模式产生线程安全的原因


img


饿汉模式


img


懒汉模式


为什么会有单线程版本和多线程版本的懒汉模式写法呢?我们来看单线程版本,如果运用到多线程的环境下,会出现什么问题:

img

在懒汉模式中,instance被声明为static,因此多个线程调用getInstance()时,返回的是同一个实例。

然而,getInstance()方法中既包含读操作(检查instance是否为null),也包含写操作(实例化instance)。

尽管赋值操作本身是原子的,但整个getInstance()方法并非原子操作。这意味着在多线程环境下,判断和赋值操作不能保证紧密执行,从而导致线程安全问题。

img

在多线程环境下,若两个线程(如 t1 和 t2)同时执行 getInstance() 方法,可能会导致值覆盖问题。

如上图,t2 线程的赋值操作可能会覆盖 t1 线程新创建的对象,导致第一个线程创建的对象被垃圾回收(GC)

这不仅增加了不必要的开销,还违背了单例模式的核心目标:避免重复创建实例,减少耗时操作,节省资源。即使第一个对象很快被释放,其创建过程中的数据加载依然会产生额外开销。


总结:

  • 饿汉模式:仅涉及对实例的读操作,不涉及写操作,因此天然线程安全。无论在单线程还是多线程环境下,其基本形式保持不变。
  • 懒汉模式:在getInstance()中包含紧密相关的读写操作(检查实例是否存在并创建实例),但这些操作无法紧密执行,导致线程安全问题。

解决单例模式的线程安全问题


面试题:

这两个单例模式的 getInstance() 在多线程环境下调用,是否会出现 bug,如何解决 bug?


1. 通过加锁让读写操作紧密执行


饿汉模式本身不存在线程安全问题,因为它仅涉及读操作,不涉及写操作。

然而,懒汉模式在多线程环境下可能出现线程安全问题,原因在于getInstance()方法中的读写操作(判断 + 赋值)不能紧密执行。

为解决这一问题,需要对相关操作进行加锁,以确保线程安全。


方法一:对方法中的读操作加锁


img

这样加锁后,如果 t1 和 t2 还出现下图读写逻辑的执行顺序:

img

  • t2 会阻塞等待 t1(或 t1 等待 t2)完成对象的创建(读写操作结束后),释放锁后,第二个线程才能继续执行。
  • 此时,第二个线程发现 instance 已非 null,会直接返回已创建的实例,不再重复创建。

方法二:对整个方法加锁


img

直接对getInstance()方法加锁,也能确保读写操作紧密执行。此时,锁对象为SingletonLazy.class。这两种方法的效果相同


2. 处理加锁引入的新问题


问题描述


对于当前懒汉模式的代码,多个线程共享一把锁,不会导致死锁。只需确保第一个线程调用getInstance()时,读写操作紧密执行即可。

后续线程在读取时发现instance != null就不会触发写操作,从而自然保证了线程安全。

img


然而,若每次调用getInstance()方法时都进行加锁解锁操作,由于synchronized是重量级锁,多次加锁,尤其是重量级锁会导致显著的性能开销,从而降低程序效率

img

拓展:


StringBuffer 就是为了解决,大量拼接字符串时,产生很多中间对象问题而提供的一个类,提供 appendinsert 方法,可以将字符串添加到,已有序列的 末尾 或 指定位置。


StringBuffer 的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上了synchronized。但是保证了线程安全是需要性能的代价的。


在很多情况下我们的字符串拼接操作,不需要线程安全,这时候 StringBuilder 登场了,
StringBuilder 是 JDK1.5 发布的, StringBuilderStringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。所以在单线程情况下,优先考虑使用 StringBuilder


StringBufferStringBuilder 二者都继承了 AbstractStringBuilder,底层都是利用可修改的 char数组 (JDK9以后是 byte 数组)。


所以如果我们有大量的字符串拼接,如果能预知大小的话最好在new StringBuffer 或者 new StringBuilder 的时候设置好 capacity ,避免多次扩容的开销(扩容要抛弃原有数组,还要进行数组拷贝创建新的数组)。


解决方法


再嵌套一次判断操作,既可以保证线程安全,又可以避免大量加锁解锁产生的开销:

img

在单线程环境下,嵌套两层相同的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,可能会出现以下问题:


内存可见性问题

核心问题

  1. 在没有 volatile 修饰时,线程 t1instance写入可能仅停留在线程本地缓存(CPU缓存或寄存器),而非立即同步到主内存
  2. 此时线程 t2 读取的可能是自己缓存中的旧值(null),即使 t1 已完成初始化。
  3. 即使 t2 进入同步块,第一次判空(if (instance == null)仍可能读取到未更新的缓存值,导致不必要的锁竞争。
  4. 第二次判空if (instance == null)t2可能会错误地认为instance == null,并再次执行实例化逻辑,导致又重复创建了新的实例。

内存可见性底层分析


  1. 硬件层面的原因
存储层级读写速度存储大小特性
寄存器最快最小(几十字节)CPU直接计算使用的临时存储
CPU缓存 (L1/L2/L3)较小(KB~MB级)每个CPU核心/多核共享,减少访问主存延迟
主内存 (RAM)大(GB级)所有线程共享,但访问速度比缓存慢100倍以上
  • 速度差异:CPU为了避免等待慢速的主内存读写,会优先使用缓存和寄存器(如将instance的值缓存在核心的L1缓存中)。
  • 副作用:线程t1修改instance后,可能仅更新了当前核心的缓存,而其他核心的缓存或主内存未被同步,导致t2读取到过期数据。

  1. Java内存模型(JMM)的抽象
  • 硬件差异被JMM抽象为 工作内存(线程私有)主内存(共享)的分离:
  • 工作内存:包含CPU寄存器、缓存等线程私有的临时存储
  • 主内存所有线程共享的真实内存

  1. 问题本质:
  • 当线程t1未强制同步(如缺少volatile或锁)时,JVM/CPU可能延迟将工作内存的修改刷回主内存,其他线程也无法感知变更。

指令重排序

指令重排序的具体问题

img

instance = new SingletonLazy() 的实际操作可分为以下步骤(可能被JVM/CPU重排序):

1. 分配对象内存空间(堆上分配,此时内存内容为默认值0/null2. 调用构造函数(初始化对象字段)
3. 将引用赋值给 instance 变量(此时 instance != null

img
可能的危险重排序

  • JVM可能将步骤 3(赋值)2(构造) 调换顺序,导致:
1. 分配内存
2. 赋值给 instance(此时 instance != null,但对象未初始化!)
3. 执行构造函数

img
这就是指令重排序问题。


  1. 多线程场景下指令重排序的后果
  • 线程 t1 执行 getInstance() 时发生重排序:
    • 先执行步骤1和3,instance 已不为 null,但对象未构造完成。
  • 线程 t2 调用 getInstance()
    • 第一次判空 if (instance == null) 会跳过
    • 若 t2 立刻调用 instance.func(),会访问未初始化的字段,导致:img
      • 空指针异常(如果 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)直接操作硬件层

  1. 写操作:强制将当前核心的缓存行(Cache Line)写回主内存,并失效其他核心的缓存。
  2. 读操作:强制从主内存重新加载数据,跳过缓存。
private static volatile SingletonLazy instance; // 通过volatile禁止缓存优化

总结

  • 直接原因:CPU缓存和寄存器的速度优化导致可见性问题。
  • 根本原因:硬件架构与编程语言内存模型的设计差异(JMM需在性能与正确性间权衡)。
  • 解决方案volatile通过内存屏障强制同步硬件层和JMM的约定。

总结:为什么双重检查锁(DCL)必须用volatile


  • 可见性:确保t1的初始化结果对t2立即可见。
  • 禁止指令重排序
    instance = new SingletonLazy() 的字节码可能被重排序为:
    1. 分配内存空间
    2. 将引用写入instance(此时instance != null但对象未初始化!)
    3. 执行构造函数
      volatile会禁止这种重排序,保证步骤2在3之后执行

4. 指令重排序问题


模拟编译器指令重排序情景


要在超市中买到左边购物清单的物品,有两种买法

img


方法一:根据购物清单的顺序买;(按照程序员编写的代码顺序进行编译)img

方法二:根据物品最近距离购买;(通过指令重排序后再编译)

img

两种方法都能买到购物清单的所有物品,但是比起第一种方法,第二种方法在不改变原有逻辑的情况下,优化执行指令顺序,更高效地执行完所有的指令


指令重排序概述


指令重排序的定义

指令重排序是指编译器或处理器为了提高性能,在不改变程序执行结果的前提下,对指令序列进行重新排序的优化技术。这种技术可以让计算机在执行指令时更高效地利用计算资源,从而提高程序的执行效率。


指令重排序的类型

  1. 编译器重排序

编译器在生成目标代码时会对源代码中的指令进行优化和重排,以提高程序的执行效率。这一过程在编译阶段完成,目的是生成更高效的机器代码。


  1. 处理器重排序

处理器在执行指令时也可以对指令进行重排序,以最大程度地利用处理器的流水线和多核等特性,从而提高指令的执行效率。


指令重排序引发的问题

尽管指令重排序可以提高程序的执行效率,但在多线程编程中可能会引发内存可见性问题。由于指令重排序可能导致共享变量的读写顺序与代码中的顺序不一致,当多个线程同时访问共享变量时,可能会出现数据不一致的情况。


指令重排序解决方案

为了解决指令重排序带来的问题,可以采取以下措施:

  1. 编译器层面:通过禁止特定类型的编译器重排序,确保指令的执行顺序符合预期。
  2. 处理器层面:通过插入内存屏障(Memory Barrier)来禁止特定类型的处理器重排序。内存屏障是一种CPU指令,用来禁止处理器指令发生重排序,从而保障指令执行的有序性。此外,内存屏障还会在处理器写入或读取值之前,将主内存的值写入高速缓存并清空无效队列,从而保障变量的可见性。

在这里插入图片描述

在这里插入图片描述


http://www.hkcw.cn/article/QLrfcvTAyF.shtml

相关文章

智慧健康养老服务与管理实训室建设方案框架:服务流程与管理模式实训

随着智慧健康养老产业的快速发展,构建契合行业需求的实训室成为培养专业人才的关键。智慧健康养老服务与管理实训室建设方案聚焦服务流程与管理模式实训,旨在通过系统化设计,让学习者在仿真场景中掌握智慧健康养老服务的全链条操作与现代化管…

KEIL 编译器高级使用与调试技巧【一】 手工编译、编译选项、预处理分析

一、手工编译bin文件 1.1 KEIL 自带的编译组件 ARM v6编译器(Arm Compiler for Embedded)包含以下几个工具: armclang 编译器和汇编器,可对 C、C 汇编文件进行编译和汇编 armlink 链接器,用于将目标文件和库文件进…

XUANYING炫影-移动版-智能轻云盒SY900Pro和SY910_RK3528芯片_免拆机通刷固件包

XUANYING炫影-移动版-智能轻云盒SY900Pro和SY910_RK3528芯片_免拆机通刷固件包 智能轻云盒SY900Pro 智能轻云盒SY910 不要刷电信版的! 不要刷电信版的! 不要刷电信版的! 固件说明: 这是一个亲测的固件。 1、默认开启 ADB功能。…

MySQL + CloudCanal + Iceberg + StarRocks 构建全栈数据服务

简述 在业务数据快速膨胀的今天,企业对 低成本存储 与 实时查询分析能力 的需求愈发迫切。 本文将带你实战构建一条 MySQL 到 Iceberg 的数据链路,借助 CloudCanal 快速完成数据迁移与同步,并使用 StarRocks 完成数据查询等操作&#xff0c…

实验设计与分析(第6版,Montgomery)第5章析因设计引导5.7节思考题5.13 R语言解题

本文是实验设计与分析&#xff08;第6版&#xff0c;Montgomery著&#xff0c;傅珏生译) 第5章析因设计引导5.7节思考题5.13 R语言解题。主要涉及方差分析&#xff0c;正态假设检验&#xff0c;残差分析&#xff0c;交互作用图。 dataframe<-data.frame( yc(36,18,30,39,20…

Excel 中的SUMIFS用法(基础版),重复项求和

1. 首先复制筛选条件所在的列&#xff0c;去除重复项目 数据 》重复项 》删除重复项 2. 输入函数公式 SUMIFS(C:C,A:A,E2) 3. 选中单元格&#xff0c;通过 ShiftF3 查看函数参数 第一个参数&#xff1a;求和区域&#xff0c;要累加的值所在的区域范围 第二个参数&#xff1a…

HCIP:MPLS静态LSP的配置及抓包

目录 一、MPLS的简单的一些知识点 1.MPLS的概述&#xff1a; 2.MPLS工作原理&#xff1a; 3.MPLS的核心组件&#xff1a; 4. MPLS标签 5.MPLS标签的处理 6.MPLS转发的概述&#xff1a; 7.MPLS的静态LSP建立方式 二、MPLS的静态LSP的实验配置 1.配置接口的地址和配置OS…

世冠科技亮相中汽中心科技周MBDE会议,共探汽车研发数字化转型新路径

近日&#xff0c;中汽中心2025年科技周MBDE前沿应用主题会议在天津成功举办。本次会议以“智汇津门共探MBDE前沿应用新征程”为主题&#xff0c;聚焦基于模型的数字工程&#xff08;MBDE&#xff09;方法论在汽车复杂系统研发中的创新实践与跨领域协同&#xff0c;旨在推动行业…

Linux之Nginx配置篇

一、Ngixn核心重点 Nginx是一款功能强大HTTP和反向代理服务器&#xff0c;目前大部分公司用于使用其Web服务器 在Nginx还未出世之前都是在使用比较传统Apache服务器&#xff0c;两则都能处理Web请求,但是后者处理效率更高、包括其负载均衡&#xff0c;反向代理功能都很强&#…

RAG混合检索:倒数秩融合RRF算法

文章目录 检索增强生成 (RAG)倒数秩融合在 RAG 中的工作原理RRF 背后的数学直觉检索增强生成 (RAG) RAG 是自然语言处理中的一种强大技术,结合了基于检索的模型和生成模型的优势。 如果检索器未能从检索器中获取相关文档,则精度较低,幻觉的可能性会增加。 有些查询适合…

2023年09月GESPC++二级真题解析(含视频)

视频讲解&#xff1a;GESP2023年9月二级C真题讲解 一、单选题 第1题 解析&#xff1a; 答案D&#xff0c;第一台用的就是电子管 第2题 解析&#xff1a; 答案B&#xff0c;x>y不符合条件&#xff0c;执行ay,bx&#xff0c;输出a、b&#xff0c;即输出y、x&#xff0c;也…

STUSB4500 PPS(PD3.0)快充SINK模块——应用 解析

0 前言 朋友参加车展&#xff0c;收获一枚很漂亮的倍思65W氮化镓快充头&#xff0c;送给我了。 我看了手中只支持33W快充的三星陷入了沉思… 快充头支持PPS协议&#xff0c;我心思这玩意适合做可调电源啊&#xff01; 上网随便一查没查到&#xff0c;都是转换成5V、9V、12V等…

4644芯片在商业航天与特种工业中的低温环境适应性研究

摘要 在现代电子技术的广泛应用中&#xff0c;商业航天和特种工业领域对于电子元件的环境适应性提出了极为苛刻的要求。本文以国科安芯研发的 ASP4644S 芯片为例&#xff0c;深入探讨其在-55℃极端低温条件下的运行性能、技术难点以及在商业航天与特种工业领域的应用。通过对芯…

Windows 权限提升 | TryHackMe | Windows Privilege Escalation

Windows 权限提升 [!quote] 权限提升就是利用用户A的主机访问权限,利用目标系统中的漏洞来获取到用户B的访问权限 Windows 服务或计划任务的配置错误账户被赋予过多权限缺少Windows安全补丁 Windows 用户 Windows系统主要存在两种类型的用户,根据访问级别,分为两类 Administ…

Redis持久化机制

一. Redis应用概述 Redis是一种高性能的KV键值对存储数据库&#xff0c;通常用作数据库、缓存和消息队列等。它支持多种数据结构&#xff0c;如字符串、哈希表、列表、集合和有序集合。Redis具有快速存取和实时响应的特点&#xff0c;广泛应用于Web开发、大数据处理和实时分析…

VMware使用时出现的问题,此文章会不断更新分享使用过程中会出现的问题

VMware使用时出现的问题&#xff0c;此文章会不断更新分享使用过程中会出现的问题 一、VMware安装后没有虚拟网卡&#xff0c;VMnet1&#xff0c;VMnet8显示黄色三角警告 此文章会不断更新&#xff0c;分享VMware使用过程中出现的问题 如果没找到你的问题可以私信我 一、VMware…

60天python训练计划----day40

DAY 40 训练和测试的规范写法 知识点回顾&#xff1a; 彩色和灰度图片测试和训练的规范写法&#xff1a;封装在函数中展平操作&#xff1a;除第一个维度batchsize外全部展平dropout操作&#xff1a;训练阶段随机丢弃神经元&#xff0c;测试阶段eval模式关闭dropout 一.单通道图…

leetcode:479. 最大回文数乘积(python3解法,数学相关算法题)

难度&#xff1a;简单 给定一个整数 n &#xff0c;返回 可表示为两个 n 位整数乘积的 最大回文整数 。因为答案可能非常大&#xff0c;所以返回它对 1337 取余 。 示例 1&#xff1a; 输入&#xff1a;n 2 输出&#xff1a;987 解释&#xff1a;99 x 91 9009, 9009 % 1337 …

vue2 + webpack 老项目升级 node v22 + vite + vue2 实战全记录

前言 随着这些年前端技术的飞速发展&#xff0c;几年前的一些老项目在最新的环境下很可能会出现烂掉的情况。如果项目不需要升级&#xff0c;只需要把编译后的文件放在那里跑而不用管的话还好。但是&#xff0c;某一天产品跑过来给你讲要升级某一个功能&#xff0c;你不得不去…

Golang | 运用分布式搜索引擎实现视频搜索业务

把前面所设计好的搜索引擎引用进来开发一个简单的具体的视频搜索业务。代码结构&#xff1a; handler目录&#xff1a;后端接口&#xff0c;负责接收请求并返回结果&#xff0c;不存在具体的搜索逻辑。video_search目录&#xff1a;具体的搜索逻辑存放在这&#xff0c;包括reca…