【多线程初阶】内存可见性问题 volatile

news2025/7/21 1:43:10

文章目录

  • 再谈线程安全问题
  • 内存可见性问题
    • 可见性问题案例
    • 编译器优化
  • volatile
  • Java内存模型(JMM)

再谈线程安全问题

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该有的结果,则说这个程序是线程安全的,反之,多线程环境中,并发执行后,产生bug就是线程不安全

在这里插入图片描述
上次谈到线程安全问题,我们介绍了前三种问题是如何解决的

  • 1.[根本问题] 操作系统对于线程的调度是随机的,抢占式执行
    这个原因我们无法解决,这是操作系统的底层设定,我们左右不了

  • 2.修改共享数据:多个线程同时对同一变量进行修改

这个原因更多的是和代码结构相关,可以调整代码结构,规避一些线程不安全的代码,但是这样的方案通用性还是不够,有些需求就是需要多线程对共享数据进行修改

  • 3.修改操作,不是原子的
    Java中解决线程安全问题最主要方案就是加锁

我们通过synchronized 关键字加锁操作来实现,锁属于系统提供的一个专门的机制,它能够产生互斥效果,通过互斥效果,把本来无序的并发执行编程一个局部上的串行执行,从而进一步解决线程安全问题

本篇文章我们解决第四种原因内存可见性问题

内存可见性问题

可见性指,一个线程对共享变量值进行修改,能够及时被其他线程看到最新值

可见性问题案例

问题代码示例:

public class Demo21 {
    private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            while(flag == 0){

            }
            System.out.println("t1 线程结束");
        });
        Thread t2 = new Thread(() ->{
            Scanner scanner =new Scanner(System.in);
            System.out.println("请输入 flag 的值:");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
在这里插入图片描述
很明显,这也是个bug ,涉及到线程安全问题,一个线程读取,一个线程修改,修改线程修改的值,并没有被读线程读到,这就是"内存可见性问题"

编译器优化

背景:
为什么要有编译器优化这样的机制呢?–>由于程序员的水平参差不齐,研究JDK的大佬们,希望通过让编译器 & JVM对程序员写的代码,自动进行优化
优点:
对于程序员原有的代码,编译器/JVM会在原有逻辑不变的前提下,对代码进行调整,使程序效率更高
缺点:

  • 编译器在编译代码时,并没有执行代码,只是根据编译的静态代码,来分析这个程序应该如何调整,才能更加高效,所以"保证原有逻辑不变",这个"保证"并非100%生效
  • 尤其在多线程中,因为并发执行随机调度的特点,执行多线程程序过程中,编译器的判断可能会出现失误,可能导致编译器的优化前后的逻辑出现细节上的偏差,

我们上述案例最后的效果并非我们预期的效果就是因为编译器优化导致的逻辑上细节有所偏差,我们接下来详细介绍是如何产生偏差的

在这里插入图片描述
上述代码中,t1 线程度循环条件 flag == 0,对其操作进行细化,会细分出两个指令,分别是读取指令(load读取flag)和比较指令(cmp)
程序会先执行读取指令load,把flag这个变量在内存中的值读取到寄存器中,才会执行比较指令cmp
就是因为load和cmp两步指令都在循环中完成且while中没有休眠限制,会在极短时间内循环多次
其中load(读内存操作)是cmp(纯CPU寄存器操作)时间开销的几千倍,因为虽然读取内存数据比读取硬盘数据要快多了,但是如果拿CPU寄存器和内存比,还是寄存器快得多

CPU寄存器模块,也可以存储一些数据
存储空间: 内存 >> 寄存器
存储速度: 内存 << 寄存器
CPU使用寄存器,是为了辅助计算,保存一些空间结果

因此执行过程中,JVM感受到 load 反复执行的结果好像都一样
JVM嘀咕,我执行那么多次读取flag的操作,发现值始终都是 0 ,既然结果都一样,既然还要反复执行那么多次,何必呢,于是编译器优化,把读取内存的操作,优化成 读取寄存器 这样的操作(把内存的值读取到寄存器了,后序在load不再重新读内存,而是直接从寄存器里取出来)
在这里插入图片描述

于是等了很多秒之后,用户真正输入新的值,真正修改 flag,此时 t1 线程已经感知不到用户的修改了 (编译器优化,使得 t1线程的读操作,不是真正的读内存)

如果稍微调整一下上述代码
在这里插入图片描述

假设本身读取 flag 的时间是 1ns 的话,如果把读内存操作优化成读寄存器 ,1ns =>0.xx ns,能把效率优化个50%以上,但是引入了 sleep之后,sleep 直接占用 1ns,此时优不优化 读内存操作,就无足轻重了

volatile

针对内存可见性问题,也不能单单指望通过sleep来解决,毕竟使用sleep会大大影响到程序的效率,我们希望不使用sleep也可以解决上述内存可见性问题

JDK大佬们知道上述内存可见性问题,在编译器优化的角度难以进行调整,就在语法中引入了 volatile 关键字,通过这个关键字来修饰某个变量,此时编译器对这个变量的读取操作,就不会被优化成读寄存器了

volatile 修饰的变量,能够保证"内存可见性"

在这里插入图片描述

前面我们讨论内存可见性时,编译器优化将读内存操作优化成直接访问工作内存(实际就是CPU的寄存器 或者 CPU的缓存),速度非常快,但是可能出现数据不一致的情况,加上volatile,强制读写内存,速度是慢了,但是数据变得更准确了
在这里插入图片描述
代码写入volatile修饰的变量

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存中刷新到主内训

代码读取volatile修饰的变量

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

注意:volatile解决的是内存可见性问题,不是解决原子性问题
volatile 和 synchronized 是有着本质区别的,synchronized能够保证原子性,volatile保证的是内存可见性

在这里插入图片描述

Java内存模型(JMM)

提到 volatile 就一定会谈到 JMM(Java Memory Model) Java内存模型

Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型

目的是屏蔽掉各种硬件和操作系统内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果

Java官方文档的术语
每个线程,有一个自己的"工作内存",同时这些线程共享同一个"主内存"
当一个线程循环进行上述读取变量操作的时候,就会把主内存中的数据,拷贝到该线程的工作内存中,后续另一个线程修改,也是先修改自己的工作内存,拷贝到主内存里
由于第一个线程仍然在读自己的工作内存,因此感知不到主内存的变化

注意:这里的术语,工作内存指的是"CPU的寄存器"

在这里插入图片描述

  • 线程之间的共享变量存储在 主内存(Main Memory)
  • 每一个线程都有自己的"工作内存"(Working Memory)
  • 当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据
  • 当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步回主内存

由于每个线程有自己的工作内存,这些工作内存中的内容相当于同一个共享变量的"副本",此时修改线程1 的工作内存中的值,线程2 的工作内存不一定会及时变化

1)初始情况下,两个线程的工作内存内容一致

在这里插入图片描述

2)一旦线程1 修改了 a 的值,此时主内存不一定能及时同步,对应的线程2 的工作内存的 a 的值也不一定能及时同步

在这里插入图片描述
此时就有了两个问题:

  • 为什么要整那么多内存?
  • 为什么要这么麻烦的拷贝来拷贝去?

1)为什么要整那么多内存?
实际上并没有这么多"内存",这只是Java规范的一个术语,属于"抽象"的 叫法,所谓的"主内存"才是真正的硬件角度的"内存",而所谓的"工作内存",则是CPU的寄存器和高速缓存

2)为什么要这么麻烦的拷贝来拷贝去?
因为CPU访问自身寄存器的速度以及高速缓存的速度,远远超过访问内存的速度(快了3-4个数量级也就是几千倍,上万倍)

比如某个代码中要连续10次读取变量的值,如果10次都从内存中读,速度是很慢的,但是如果只是第一次从内存读,读到的结果缓存到CPU的某个寄存器中,那么后9次读数据就不必直接访问内存了,效率就大大提高了

那么接下来的问题又来了,既然访问寄存器速度这么快,还要内存干嘛?
一个字:贵
在这里插入图片描述

值的一提的是,快和慢都是相对的,CPU访问寄存器速度远远快于内存,但是内存的访问速度又远远快于硬盘.对应的 CPU价格最贵,内存次之,硬盘最便宜

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2397148.html

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

相关文章

C++ 类模板三参数深度解析:从链表迭代器看类型推导与实例化(为什么迭代器类模版使用三参数?实例化又会是怎样?)

本篇主要续上一篇的list模拟实现遇到的问题详细讲解&#xff1a;<传送门> 一、引言&#xff1a;模板参数的 "三角锁钥" 在 C 双向链表实现中&#xff0c;__list_iterator类模板的三个参数&#xff08;T、Ref、Ptr&#xff09;如同精密仪器的调节旋钮&#x…

MySQL强化关键_018_MySQL 优化手段及性能分析工具

目 录 一、优化手段 二、SQL 性能分析工具 1.查看数据库整体情况 &#xff08;1&#xff09;语法格式 &#xff08;2&#xff09;说明 2.慢查询日志 &#xff08;1&#xff09;说明 &#xff08;2&#xff09;开启慢查询日志功能 &#xff08;3&#xff09;实例 3.s…

ASP.NET MVC添加模型示例

ASP.NET MVC高效构建Web应用ASP.NET MVC 我们总在谈“模型”&#xff0c;那到底什么是模型&#xff1f;简单说来&#xff0c;模型就是当我们使用软件去解决真实世界中各种实际问题的时候&#xff0c;对那些我们关心的实际事物的抽象和简化。比如&#xff0c;我们在软件系统中设…

【Part 3 Unity VR眼镜端播放器开发与优化】第二节|VR眼镜端的开发适配与交互设计

文章目录 《VR 360全景视频开发》专栏Part 3&#xff5c;Unity VR眼镜端播放器开发与优化第一节&#xff5c;基于Unity的360全景视频播放实现方案第二节&#xff5c;VR眼镜端的开发适配与交互设计一、Unity XR开发环境与设备适配1.1 启用XR Plugin Management1.2 配置OpenXR与平…

第1天:认识RNN及RNN初步实验(预测下一个数字)

RNN&#xff08;循环神经网络&#xff09; 是一种专门设计用来处理序列数据的人工神经网络。它的核心思想是能够“记住”之前处理过的信息&#xff0c;并将其用于当前的计算&#xff0c;这使得它非常适合处理具有时间顺序或上下文依赖关系的数据。 核心概念&#xff1a;循环连…

树莓派安装openwrt搭建软路由(ImmortalWrt固件方案)

&#x1f923;&#x1f449;我这里准备了两个版本的openwrt安装方案给大家参考使用&#xff0c;分别是原版的OpenWrt固件以及在原版基础上进行改进的ImmortalWrt固件。推荐使用ImmortalWrt固件&#xff0c;当然如果想直接在原版上进行开发也可以&#xff0c;看个人选择。 &…

电子电气架构 --- 如何应对未来区域式电子电气(E/E)架构的挑战?

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

易学探索助手-个人记录(十二)

近期我完成了古籍处理板块页面升级&#xff0c;补充完成原文、句读、翻译的清空、保存和编辑&#xff08;其中句读仅可修改标点&#xff09;功能&#xff0c;新增原文和句读的繁简体切换功能 一、古籍处理板块整体页面升级 将原来一整个页面呈现的布局改为分栏呈现&#xff0…

Python窗体编程技术详解

文章目录 1. Tkinter简介示例代码优势劣势 2. PyQt/PySide简介示例代码(PyQt5)优势劣势 3. wxPython简介示例代码优势劣势 4. Kivy简介示例代码优势劣势 5. PySimpleGUI简介示例代码优势劣势 技术对比总结选择建议 Python提供了多种实现图形用户界面(GUI)编程的技术&#xff0c…

NVMe协议简介之AXI总线更新

更新AXI4总线知识 AXI4总线协议 AXI4总线协议是由ARM公司提出的一种片内总线协议 &#xff0c;旨在实现SOC中各模块之间的高效可靠的数据传输和管理。AXI4协议具有高性能、高吞吐量和低延迟等优点&#xff0c;在SOC设计中被广泛应用 。随着时间的推移&#xff0c;AXI4的影响不…

设计模式——责任链设计模式(行为型)

摘要 责任链设计模式是一种行为型设计模式&#xff0c;旨在将请求的发送者与接收者解耦&#xff0c;通过多个处理器对象按链式结构依次处理请求&#xff0c;直到某个处理器处理为止。它包含抽象处理者、具体处理者和客户端等核心角色。该模式适用于多个对象可能处理请求的场景…

基于Android的医院陪诊预约系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

基于Spring Boot 电商书城平台系统设计与实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…

【金融基础学习】债券回购方式

债券回购作为货币市场的重要工具&#xff0c;本质上是一种以债券为抵押的短期资金借贷行为。在银行间市场&#xff0c;质押式回购与**买断式回购*是两种主要形式。 1. 质押式回购(Pledged Repo, RP) – 所有权不转移的短期融资工具 1.1 质押式回购概述 质押式回购是交易双方…

第五十九节:性能优化-GPU加速 (CUDA 模块)

在计算机视觉领域,实时性往往是关键瓶颈。当传统CPU处理高分辨率视频流或复杂算法时,力不从心。本文将深入探索OpenCV的CUDA模块,揭示如何通过GPU并行计算实现数量级的性能飞跃。 一、GPU加速:计算机视觉的必由之路 CPU的强项在于复杂逻辑和低延迟任务,但面对图像处理中高…

单元测试-概述入门

目录 main方法测试缺点&#xff1a; 在pom.xm中&#xff0c;引入junit的依赖。,在test/java目录下&#xff0c;创建测试类&#xff0c;并编写对应的测试方法&#xff0c;并在方法上声明test注解。 练习&#xff1a;验证身份证合法性 测试成功 测试失败 main方法测试缺点&am…

⚡ Hyperlane —— 比 Rocket 更快的 Rust Web 框架!

⚡ Hyperlane —— 比 Rocket 更快的 Rust Web 框架&#xff01; 在现代 Web 服务开发中&#xff0c;开发者需要一个既轻量级又高性能的 HTTP 服务器库来简化开发流程&#xff0c;同时确保服务的高效运行。Hyperlane 正是为此而生——一个专为 Rust 开发者设计的 HTTP 服务器库…

《AI Agent项目开发实战》DeepSeek R1模型蒸馏入门实战

一、模型蒸馏环境部署 注&#xff1a;本次实验仍然采用Ubuntu操作系统&#xff0c;基本配置如下&#xff1a; 需要注意的是&#xff0c;本次公开课以Qwen 1.5-instruct模型为例进行蒸馏&#xff0c;从而能省略冷启动SFT过程&#xff0c;并且 由于Qwen系列模型本身性能较强&…

字节golang后端二面

前端接口使用restful格式&#xff0c;post与get的区别是什么&#xff1f; HTTP网络返回的状态码有哪些&#xff1f; go语言切片与数组的区别是什么&#xff1f; MySQL实现并发安全避免两个事务同时对一个记录写操作的手段有哪些&#xff1f; 如何实现业务的幂等性&#xff08;在…

vscode + cmake + ninja+ gcc 搭建MCU开发环境

vscode cmake ninja gcc 搭建MCU开发环境 文章目录 vscode cmake ninja gcc 搭建MCU开发环境1. 前言2. 工具安装及介绍2.1 gcc2.1.1 gcc 介绍2.1.2 gcc 下载及安装 2.2 ninja2.2.1 ninja 介绍2.2 ninja 安装 2.3 cmake2.3.1 cmake 介绍2.3.2 cmake 安装 2.4 VScode 3. 上手…