当前位置: 首页 > news >正文

【多线程-从零开始-伍】volatile关键字和内存可见性问题

volatile 关键字

import java.util.Scanner;  public class Demo2 {  private static int n = 0;  public static void main(String[] args) {  Thread t1 = new Thread(() -> {  while(n == 0){  //啥都不写  }  System.out.println("t1 线程结束循环");  }, "t1");  Thread t2 = new Thread(() -> {  Scanner scanner = new Scanner(System.in);  System.out.println("请输入一个整数:");  n = scanner.nextInt();  }, "t2");        t1.start();  t2.start();  }
}
  • 当我们输入一个非 0 的数,理应 t1 中循环条件就不成立,将会打印“线程结束循环”,但实际上输入 1 后,t1 没有任何动静
  • 我们通过 jconsole 可以看到 t1 线程仍是持续工作的image.png|353
  • 上述问题的原因,就是“内存可见性问题

内存可见性问题

层次空间速度成本数据
CPU 寄存器掉电后丢失
内存中等中等中等掉电后丢失
硬盘掉电后不丢失
while(n == 0) {}
  • 上面代码中的这个操作,循环会执行非常多次,每次循环,都要执行一个 n == 0 这样的判定
    1. 从内存读取数据到寄存器中(读取内存,相比之下,这个操作的速度非常慢)
    2. 通过类似 cmp 指令,比较寄存器和 0 的值(这个指令执行速度非常快)
  • 此时 JVM 执行这个代码的过程的时候,发现:每次执行循环操作的开销非常大,并且每次执行的结果都是一样的
  • 并且 JVM 根被没有意识到,用户可能在未来会修改 n,于是 JVM 就做了一个大胆的操作——直接把这个操作给优化掉了
    • 每次循环,不会重新读取内存中的数据,而是直接读取寄存器/cache 中的数据(缓存的结果)
  • JVM 做出上述决定之后,此时意味着循环的开销大幅度降低了,但当用户修改 n 的时候,内存中的 n 已经改变了,但是由于 t1 线程每次循环,不会真的读内存,所以感知不到 n 的改变
  • 内存中的 n 的改变,对于线程 t1 来说是“不可见的”,这样就引起了 bug
  • 内存可见性问题本质上是编译器/JVM 对代码进行优化的时候,优化出了 bug
  • 如果代码是单线程的,编译器/JVM 的代码优化一般都是非常准确的,优化之后,不会影响到逻辑
  • 但是代码如果是多线程的,编译器/JVM 的代码优化就可能出现误判(编译器/JVM 的 bug),导致不该优化的地方也给优化了,于是就造成了内存可见性问题

[!quote] 编译器问啥要做优化?

  • 有些程序员写出来的代码太低效了,为了能降低程序员的门槛,即使你的代码写的一般,最终执行也不会落下风
  • 因此一些主流的编译器,都会好引入优化机制(优化手段是多种多样的)
  • 优化就是编译器自动调整你写的代码,保持原有逻辑不变的前提下,提高代码的执行效率
  • 代码优化的效果是非常明显的

  • 若一个服务器在开启优化的时候启动时间为 10 min,那么在不开启优化的时候,启动时间可能会在 30 min+

若在 while 循环中加入一个 sleep 操作

while(n == 0) {Thread.sleep(10);
}
System.out.println("t1 线程结束循环");//在输入1后,成功输出:"t1 线程结束循环"
  • 说明加入 sleep 之后,刚才谈到的针对读取 n 内存数据的优化操作不再进行了
  • 因为和读取内存相比,sleep 的开销更大,远远超过了读取内存,就算把读取内存的操作优化掉,也没有意义,杯水车薪

volatile 关键字的用法

  • volatile 关键字修饰一个变量,提示编译器说这个变量是“易变”的
  • 编译器进行上述优化的前提,是编译器认为,针对这个变量的频繁读取,结果都是固定的
  • 使用 volatile 关键字修饰变量之后,编译器就会禁止上述的优化,确保每次循环都是从内存中重新读取数据
private static volatile int n = 0;
  • 编译器的开发者,知道这个场景中可能出现误判,于是就把权限交给程序员,让程序员能够部分的干预到优化的进行
  • 引入 volatile 的时候,编译器生成这个代码的时候,就会给这个变量的读取操作附近生成一些特殊的指令,称为“内存屏障”,后续 JVM 执行到这些特殊指令,就知道不能进行上述优化了

volatile 只是解决内存可见性问题,不能解决原子性问题,如果两个线程针对同一个变量进行修改(count++),volatile 也无能为力

[!quote] 网络上“内存可见性”问题:

  • 工作内存(其实就是 CPU 的寄存器和 cache)
  • 主内存

  • 整个 Java 程序持有这个主内存,每个 Java 程序又有一份自己的工作内存
  • 像上述例子中的内存变量 n,本身是在主内存中,在 t1 和 t2 线程工作的过程中,就会把主内存的数据拷贝到>工作内存中
  • t2 如果修改了 n,先修改工作内存,再写回到主内存中。t1 读取 n 的时候,则是从主内存加载到工作内存,接下来的判定都是依照工作内存的值来进行判定的。此时 t2 修改了主内存,对于 t1 的工作内存未产生影响,从而出现了上述内存可见性问题

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • OD C卷 - 多线段数据压缩
  • 【PCA提取主要特征通俗】
  • Unity 功能 之 创建 【Unity Package】 Manager 自定义管理的包使用配置URL,使用 git URL加载的简单整理
  • Java SpringBoot 集成 MinIO 资料
  • RabbitMq架构原理剖析及应用
  • 【PostgreSQL教程】PostgreSQL UPDATE 语句
  • 数据库的基础的exists
  • java中字面量和golang中字面量区别
  • 掌握Java并发编程基础
  • 计算机网络(网络层)
  • 基于RFID技术的智能压缩机装配线优化方案
  • 2024年有哪些好用的文件加密软件?十款常用加密软件推荐
  • Docker 环境下使用 Traefik v3 和 MinIO 快速搭建私有化对象存储服务
  • Ubuntu 22.04 Docker安装笔记
  • python3 pyside6图形库学习笔记及实践(四)
  • Android组件 - 收藏集 - 掘金
  • eclipse的离线汉化
  • gf框架之分页模块(五) - 自定义分页
  • javascript 哈希表
  • JavaScript中的对象个人分享
  • js操作时间(持续更新)
  • mockjs让前端开发独立于后端
  • RxJS 实现摩斯密码(Morse) 【内附脑图】
  • Spring技术内幕笔记(2):Spring MVC 与 Web
  • 对JS继承的一点思考
  • 记一次和乔布斯合作最难忘的经历
  • 盘点那些不知名却常用的 Git 操作
  • 浅谈Golang中select的用法
  • 网络应用优化——时延与带宽
  • 写代码的正确姿势
  • 如何在 Intellij IDEA 更高效地将应用部署到容器服务 Kubernetes ...
  • 微龛半导体获数千万Pre-A轮融资,投资方为国中创投 ...
  • 新海诚画集[秒速5センチメートル:樱花抄·春]
  • ​io --- 处理流的核心工具​
  • ​iOS实时查看App运行日志
  • ‌移动管家手机智能控制汽车系统
  • #我与Java虚拟机的故事#连载05:Java虚拟机的修炼之道
  • (3)STL算法之搜索
  • (C语言)逆序输出字符串
  • (php伪随机数生成)[GWCTF 2019]枯燥的抽奖
  • (附源码)spring boot基于小程序酒店疫情系统 毕业设计 091931
  • (力扣记录)1448. 统计二叉树中好节点的数目
  • (每日一问)基础知识:堆与栈的区别
  • (三分钟)速览传统边缘检测算子
  • (四)事件系统
  • (一) springboot详细介绍
  • (一)80c52学习之旅-起始篇
  • (转)iOS字体
  • (转)拼包函数及网络封包的异常处理(含代码)
  • (转)原始图像数据和PDF中的图像数据
  • ****Linux下Mysql的安装和配置
  • .a文件和.so文件
  • .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)...
  • .NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)...
  • .NET6实现破解Modbus poll点表配置文件