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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


单例模式


单例模式确保某个类在程序中只有一个实例,避免多次创建实例(禁止多次使用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.pswp.cn/news/908213.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2. 库的操作

2.1 创建数据库 语法: CREATE DATABASE [IF NOT EXISTS] db_name [create_specification [, create_specification] ...] create_specification: [DEFAULT] CHARACTER SET charset_name # 字符集: 存储编码 [DEFAULT] COLLATE collation_name # 校验集: 比较/选择/读…

道可云人工智能每日资讯|北京农业人工智能与机器人研究院揭牌

道可云人工智能&元宇宙每日简报(2025年6月3日)讯,今日人工智能&元宇宙新鲜事有: 北京农业人工智能与机器人研究院揭牌 5月30日,北京市农业农村局、北京市海淀区人民政府、北京市农林科学院共同主办北京农业人…

【JSON-to-Video】设置背景视频片断

目录 设置bgVideo字段 1. 设置bgVideo.videoList字段 2. 设置randomPlay字段 3. 设置complete字段 4. 调用API,制作视频 欢迎来到JSON转视频系列教程。今天要教大家如何添加背景视频片断,在视频制作中,巧妙运用背景视频,能为…

星闪开发之Server-Client 指令交互控制红灯亮灭案例解析(SLE_LED详解)

系列文章目录 星闪开发之Server-Client 指令交互控制红灯亮灭的全流程解析(SLE_LED详解) 文章目录 系列文章目录前言一、项目地址二、客户端1.SLE_LED_Client\inc\SLE_LED_Client.h2.SLE_LED_Client\src\SLE_LED_Client.c头文件与依赖管理宏定义与全局变…

Linux shell练习题

Shell 1. 判断~/bigdata.txt 是否存在,若已存在则打印出”该文件已存在“,如不存在,则输出打印:”该文件不存在“ if [ -f ./bigdata.txt ];then echo "文件存在" else echo "文件不存在" fi2. 判断~/bigd…

Linux基本指令(三)

接上之前的文章,咱继续分享Linux的基本指令,Linux指令比较多,很难全部记住需要做笔记对常用的指令进行记录,方便以后复习查找,做笔记也可以对知识理解更加深刻。 目录 时间相关指令 date显示 时间戳 cal指令 ​编…

WebRTC中sdp多媒体会话协议报文详细解读

sdp介绍 在WebRTC(Web实时通信)中,SDP(Session Description Protocol)是用来描述和协商多媒体会话的协议。它定义了会话的参数和媒体流的信息,如音视频编码格式、传输方式、网络地址等。SDP是WebRTC中一个…

【MySQL】 约束

一、约束的定义 MySQL 约束是用于限制表中数据的规则,确保数据的 准确性 和 一致性 。约束可以在创建表时定义,也可以在表创建后通过修改表结构添加。 二、常见的约束类型 2.1 NOT NULL 非空约束 加了非空约束的列不能为 NULL 值,如果可以…

【.net core】【watercloud】树形组件combotree导入及调用

源码下载:combotree: 基于layui及zTree的树下拉框组件 链接中提供了组件的基本使用方法 框架修改内容 1.文件导入(路径可更具自身情况自行设定) 解压后将文件夹放在图示路径下,修改文件夹名称为combotree 2.设置路径(设置layu…

ES101系列07 | 分布式系统和分页

本篇文章主要讲解 ElasticSearch 中分布式系统的概念,包括节点、分片和并发控制等,同时还会提到分页遍历和深度遍历问题的解决方案。 节点 节点是一个 ElasticSearch 示例 其本质就是一个 Java 进程一个机器上可以运行多个示例但生产环境推荐只运行一个…

CppCon 2015 学习:3D Face Tracking and Reconstruction using Modern C++

1. 3D面部追踪和重建是什么? 3D面部追踪(3D Face Tracking): 实时检测并追踪人脸在三维空间中的位置和姿态(如转头、点头、表情变化等),通常基于摄像头捕获的视频帧。3D面部重建(3D…

代码中的问题及解决方法

目录 YOLOX1. AttributeError: VOCDetection object has no attribute cache2. ValueError: operands could not be broadcast together with shapes (8,5) (0,)3. windows远程查看服务器的tensorboard4. AttributeError: int object has no attribute numel YOLOX 1. Attribu…

【JVM】Java类加载机制

【JVM】Java类加载机制 什么是类加载? 在 Java 的世界里,每一个类或接口在经过编译后,都会生成对应的 .class 字节码文件。 所谓类加载机制,就是 JVM 将这些 .class 文件中的二进制数据加载到内存中,并对其进行校验…

vue的监听属性watch的详解

文章目录 1. 监听属性 watch2. 常规用法3. 监听对象和route变化4. 使用场景 1. 监听属性 watch watch 是一个对象,键是需要观察的表达式,用于观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数的参数是新值和旧值。值也可以是方法名&am…

如何在 Ubuntu 24.04 服务器上安装 Apache Solr

Apache Solr 是一个免费、开源的搜索平台,广泛应用于实时索引。其强大的可扩展性和容错能力使其在高流量互联网场景下表现优异。 Solr 基于 Java 开发,提供了分布式索引、复制、负载均衡及自动故障转移和恢复等功能。 本教程将指导您如何在 Ubuntu 24.…

Linux内核中TCP三次握手的实现机制详解

TCP三次握手是建立可靠网络连接的核心过程,其在内核中的实现涉及复杂的协议栈协作。本文将深入分析Linux内核中三次握手的实现机制,涵盖客户端与服务端的分工、关键函数调用、协议号验证及数据包处理流程。 一、三次握手的整体流程 三次握手分为三个阶段,客户端与服务端通过…

服务器--宝塔命令

一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…

优化 Spring Boot API 性能:利用 GZIP 压缩处理大型有效载荷

引言 在构建需要处理和传输大量数据的API服务时,响应时间是一个关键的性能指标。一个常见的场景是,即使后端逻辑和数据库查询已得到充分优化,当API端点返回大型数据集(例如,数千条记录的列表)时&#xff0…

【WPF】WPF 项目实战:构建一个可增删、排序的光源类型管理界面(含源码)

💡WPF 项目实战:构建一个可增删、排序的光源类型管理界面(含源码) 在实际的图像处理项目中,我们经常需要对“光源类型”进行筛选或管理。今天我们来一步步构建一个实用的 WPF 界面,实现以下功能&#xff1…

C++23 已弃用特性

文章目录 1. std::aligned_storage 与 std::aligned_union1.1 特性介绍1.2 被弃用的原因1.3 替代方案 2. std::numeric_limits::has_denorm2.1 特性介绍2.2 被弃用的原因 3. 总结 C23 已弃用特性包括:std::aligned_storage、std::aligned_union 与 std::numeric_lim…