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

指令重排序带来的多线程问题与volatile解决方案

指令重排序是编译器、JVM 和 CPU 优化性能的常见手段。
为了提高执行效率,编译器、JVM、CPU可能会在不影响单线程语义的前提下,改变代码执行的顺序。
但它可能会导致并发程序出现不可预测或错误的行为。

举个例子:

int a = 1; // A
int b = 2; // B
c = a + b; // C

你以为执行顺序是A → B,但编译器可能会调换它们的顺序,或者 CPU 同时执行它们。不管是并行执行还是真的改变顺序,都是指令重排序。不过 C总得最后执行。因为 C有对 A B 的依赖关系。

系统的多级优化可能会带来三种重排序:
1、编译器优化重排序
Java 编译器会在生成字节码或机器码时重排指令,只要不影响单线程的执行结果,它就认为是合法的。
就比如上面的例子里,如果 A 和 B 无依赖关系,编译器可能会调换顺序。

2、CPU 执行时的重排序(乱序执行)
现代 CPU(如 Intel、ARM)都支持乱序执行,即同时执行多条不依赖的指令,然后乱序提交,这会导致执行顺序与代码顺序不一致。

3、内存系统的可见性乱序(多线程)
由于缓存、写缓冲区、总线延迟等原因,内存写入的可见性顺序可能被打乱。也就是说,线程 A 写入的值,线程 B 可能“晚一点”才能看到。

不过这只做了解,重要的是:
单线程下,重排序是优化,非常的OK;
但是多线程下,还重排,就可能出问题。

多线程

多线程的情况下,假设x 和flag 都是全局变量,初始值是0 和false 。

// 线程1
x = 1;          // 语句A
flag = true;    // 语句B// 线程2
if (flag) {     // 语句CSystem.out.println(x);  // 语句D
}

原计划:原本线程1的计划是当 x=1 之后,再把 flag 设为 true,flag 类似于释放锁,然后线程2才能拿到flag 锁,并打印x,所以两个线程原计划是线程2打印的是1。
重排序:但是因为JMM的指令重排序,语句 B 可能先于语句 A 执行,语句 C 察觉到语句 B 的执行之后,就会打印 x,此时语句 A 还没有执行,所以 x 还是旧的值,就出现了乱套。

volatile禁止指令重排序

如果用 volatile 修饰了 flag,就可以解决这个问题。

volatile boolean flag = false;
// 线程1
x = 1;          // 普通写
flag = true;     // volatile 写,确保 x=1 对线程2可见// 线程2
if (flag) {      // volatile 读,确保看到线程1的所有写操作System.out.println(x);  // 保证输出 1
}

给 flag 修饰 volatile 之后,对于他的写操作会建立 happens-before 关系,确保多线程的正确性。具体来说就是,当 flag 被声明为 volatile 时,会插入内存屏障,包括写屏障和读屏障:
写屏障(StoreStore + StoreLoad):
在 flag=true 之前的所有普通写(如 x=1)必须先刷回主内存。
禁止 x=1 和 flag=true 的重排序。

读屏障(LoadLoad + LoadStore):
在 if (flag) 时,会强制从主内存重新加载 flag 和 之前的所有变量(包括 x)。

这就保障了flag=true 之前x=1 必须已经执行完毕,并且不能只存在本地内存,要刷回主内存。
而且由于写屏障,并没有被volatile修饰的x=1 也会直接刷回主内存,也保证其他线程可见性。

总结就是:
volatile的作用:
①可以防止变量写的指令重排序
写入 volatile 变量时,JVM 会在底层插入 StoreStore 屏障,禁止该 volatile 写与它前面的普通写重排序。

读 volatile 变量时,会插入 LoadLoad + LoadStore 屏障,禁止后续读写操作提前执行。

②可以保证可见性
当一个线程修改了 volatile 变量的值:必须立即刷新到主内存;

当另一个线程读取该 volatile 变量时:必须从主内存重新读取,不能使用工作内存的旧值;

此外:
所有在 写 volatile 变量之前的普通写操作,也会被一起同步到主内存。
所有在 读 volatile 变量之后的普通读操作,也必须重新从主内存读取(如果有数据依赖)。

再总结就是:
volatile 保证可见性和部分有序性(禁止特定的重排序),但不保证原子性。

http://www.dtcms.com/a/274159.html

相关文章:

  • Linux设备树(dts/dtsi/dtb、设备树概念,设备树解析,驱动匹配)
  • P1204 [USACO1.2] 挤牛奶Milking Cows
  • 如何设置直播间的观看门槛,让直播间安全有效地运行?
  • 云原生周刊:镜像兼容性
  • 假日流量红利:如何用ASO策略抢占季节性下载高峰?
  • 不同质押周期对代币价格稳定性的具体影响及数据支撑
  • MinIO文件存储服务工具详细使用指南
  • 和服腰封改造:3种解构主义造型的东方美学新解
  • 2025年亚太中文赛赛题浅析-助攻快速选题
  • 【氮化镓】100 V GaN晶体管在关态应力下的双退化
  • Spring Boot中请求参数读取方式
  • HTTP 请求方法详解:GET、POST、PUT、DELETE 等
  • Python中类静态方法:@classmethod/@staticmethod详解和实战示例
  • LeetCode 278. 第一个错误的版本
  • 基于生产者消费者模型的线程池【Linux操作系统】
  • mysql中的自增ID
  • 物联网-ESP8266
  • API、MCP Client、MCP Server、LLM之间的业务逻辑关系
  • 医疗预约系统中的录音与图片上传功能实现:Vue3+Uniapp 实战
  • 在线重装 Proxmox VE
  • Swift中SwiftyJSON使用详情
  • 墙裂推荐!McpStore库三行代码为Agent添加MCP能力
  • 业务建模如何让金融数字化转型 “轻” 装上
  • CentOS7环境安装包部署并配置MySQL5.7
  • 什么是proxy
  • 使用浏览器inspect调试wx小程序
  • 构建基于表单配置的 Jenkins 测试项目(接口、UI、APP、Jmeter)
  • 加速市场反馈,助力产品迭代升级​
  • 如何使用 Python 删除 Excel 中的行、列和单元格 – 详解
  • IAR携手矽力杰与普华基础软件,共推RISC-V车规芯片高安全应用落地