深刻理解JAVA并发中的有序性问题和解决之道

news2024/12/31 5:23:20

问题

Java并发情况下总是会遇到各种意向不到的问题,比如下面的代码:

int num = 0;

boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
 if(ready) {
 	r.r1 = num + num;
 } else {
 	r.r1 = 1;
 }
}
// 线程2 执行此方法
public void actor2(I_Result r) { 
 num = 2;
 ready = true; 
}
  • 线程1中如果发现ready=true,那么r1的值等于num + num,否则等于1,然后将结果保存到I_Result对象中

  • 线程2中先修改num=2,然后设置ready=true

那大家觉得I_Result中的r1值可能是多少呢?

  1. r1值等于4, 这个大家都能想到, CPU先执行了线程2,然后执行线程1

  2. r1值等于1,这个也容易理解,CPU先执行了线程1,然后执行线程2

  3. 那我如果说r1值有可能等于0,大家可能觉得离谱,不信的话,我们验证下。

压测验证结果

由于并发问题出现的概率比较低,我们可以使用openjdk提供的jcstress框架进行压测,就能够出现各种可能的情况。

jcstress:全名The Java Concurrency Stress tests,是一个实验工具和一套测试工具,用于帮助研究JVM、类库和硬件中并发支持的正确性。详细使用可以参考文章:并发测试工具:jcstress - 秋风飒飒吹 - 博客园

  1. 生成压测工程

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=com.alvin -DartifactId=juc-order -Dversion=1.0

生成的工程代码如下图:

  1. 填充测试内容

  • 方法actor1是压测第一个线程干的活,将结果保存到I_Result中。

  • 方法actor2是压测第二个线程干的活

  • 类前面的@Outcome注解用来展示验证结果,特别是id="0"这个是我们感兴趣的结果

  1. 运行压测工程

mvn clean install 
java -jar target/jcstress.jar
  1. 查看运行结果

运行结果如下图所示:

  • 有4000多次出现了0的结果

  • 大部分情况的结果还是1和4

你是不是还是很困惑,其实这就是并发执行的一些坑,我们下面来解释下原因。

原因分析

如果先要出现r1的值等于0,那么有一个可能0+0=0,那么也就是num=0

你可能想num怎么可能等于0,代码逻辑明明是先设置num=2,然后才修改ready=true, 最后才会走到num+num 的逻辑啊....

在并发的世界里,我们千万不要被固有的思维限制了,那是不是有可能num=2ready=true的执行顺序发生了变化呢。如果你想到这里,也基本接近真相了。

原因: JAVA中在指令不存在依赖的情况下,会进行顺序的调整,这种现象叫做指令重排序,是 JIT 编译器在运行时的一些优化。这也是为什么出现0的根本原因。

指令重排不会影响单线程执行的结果,但是在多线程的情况下,会有个可能出现问题。

理解指令重排序

前面提到出现问题的原因是因为指令重排序,你可能还是不大理解指令重排序究竟是什么,以及它的作用,那我这边用一个鱼罐头的故事带大家理解下。

我们可以把工人当做CPU,鱼当做指令,工人加工一条鱼需要 50 分钟,如果一条鱼、一条鱼顺序加工,这样是不是比较慢?

没办法得优化下,不然要喝西北风了,发现每个鱼罐头的加工流程有 5 个步骤:

  • 去鳞清洗 10分钟

  • 蒸煮沥水 10分钟

  • 加注汤料 10分钟

  • 杀菌出锅 10分钟

  • 真空封罐 10分钟

每个步骤中也是用到不同的工具,那能否可以并行呢?如下图所示:

我们发现中间用很多步骤是并行做的,大大的提高了效率。但是在并行加工鱼的过程中,就会出现顺序的调整,比如先做第二条的鱼的某个步骤,然后在做第一条鱼的步骤。

现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的 CPU,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为五级指令流水线。CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率。

处理器在进行重排序时,必须要考虑指令之间的数据依赖性

  • 单线程环境也存在指令重排,由于存在依赖性,最终执行结果和代码顺序的结果一致

  • 多线程环境中线程交替执行,由于编译器优化重排,会获取其他线程处在不同阶段的指令同时执行

volatile关键字

那么对于上面的问题,如何解决呢?

使用volatile关键字。

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • volatile 变量的写指令后会加入写屏障

  • volatile 变量的读指令前会加入读屏障

内存屏障本质上是一个CPU指令,形象点理解就是一个栅栏,拦在那里,无法跨越。

内存屏障分为写屏障和读屏障,有什么有呢?

  1. 保证可见性

  • 写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中

  • 读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

  1. 保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

回到前面的问题,如果对ready加了volatile以后,那么num=2就无法到后面去了,同样读取也是,如上图所示。

final底层也是通过内存屏障实现的,它与volatile一样。

  • 对final变量的写指令加入写屏障。也就是类初始化的赋值的时候会加上写屏障。

  • 对final变量的读指令加入读屏障。加载内存中final变量的最新值。

总结

JAVA并发中的有序性问题其实比较难理解,本文通过一个例子验证了并发情况下会出现有序性的问题,从而引发意想不到的结果。这个主要的原因是为了提高性能,指令会发生重排序导致的。为了解决这样的问题,我们可以使用volatile这个关键字修饰变量,它能够保证有序性和可见性,但是无法保证原子性。如果以后遇到一些成员变量或者静态变量就要特别注意了,需要分析并发情况下会有哪些问题。

如果本文对你有帮助的话,请留下一个赞吧

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

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

相关文章

Clickhouse 使用DBeaver连接

ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)。 据处理大致可以分成两大类:联机事务处理OLTP(on-line transaction processing)、联机分析处理OLAP(On-Line Analytical Processing)。 OLTP是传统的…

JavaWeb--JDBC核心技术

JavaWeb--JDBC核心技术JDBC核心技术第1章:JDBC概述1.1 数据的持久化1.2 Java中的数据存储技术1.3 JDBC介绍1.4 JDBC体系结构1.5 JDBC程序编写步骤第2章:获取数据库连接2.1 要素一:Driver接口实现类2.1.1 Driver接口介绍2.1.2 加载与注册JDBC驱…

Redis学习笔记(六)

哨兵 哨兵时一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的master,并将所有slave连接到新的master哨兵的作用 监控 不断检查master和slave是否正常运行master存活检测、master与slave运行情况检测 通…

Linux调度(三)——抢占式调度

目录 抢占式场景一: 抢占式场景二 抢占的时机 用户态的抢占时机 抢占式机一: 抢占时机二: 内核态的抢占时机 时机一 时机二 总结 之前讲了主动式调度,就是进程运行到一半,因为等待I/O等操作而主动让出CPU&a…

动态规划算法(3)(不同方案数问题+拆分问题)

文章目录不同路径不同路径II整数拆分不同的二叉搜索树动态规划解题五步走: 确定dp数组以及下标的含义确定递推公式dp数组如何初始化确定遍历顺序举例推导dp数组 不同路径 力扣传送门: https://leetcode.cn/problems/unique-paths/description/ 确定dp…

[附源码]JAVA毕业设计酒店订房系统(系统+LW)

[附源码]JAVA毕业设计酒店订房系统(系统LW) 目运行 环境项配置: Jdk1.8 Tomcat8.5 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术&…

基于opencv答题卡识别基本处理_1

文章目录1.读取图片2.图片预处理2.1 原图转换为灰度图2.2 高斯滤波处理,去除噪声点2.3 增强亮度2.4 自适应二值化2.5 图片可视化3. 添加边框3.1 使用copyMakeBorder添加边框3.2 可视化图片查看效果3.3 手动截取答题卡区域1.读取图片 img cv2.imread(images/5.png)…

Nx C++程序使用spdlog库进行日志存储

1 spdlog简介 spdlog是一个开源的日志库,在github上有。代码见这里,文档这里 C语言的,支持Linux、windows等系统。 csdn上也有许多介绍,这里列举两个:1、2 2 使用 2.1下载编译链接 有多种使用方式,这里…

(三)沟通管理风险管理采购管理@相关方管理

沟通管理目录概述需求:设计思路实现思路分析1.沟通管理绩效报告提供资源2.管理沟通3.监督沟通风险管理规划风险管理识别风险定性风险分析:定量分析风险规划风险应对实施分享应对监督风险采购管理:12.1 规划采购的管理12.2 实施采购控制采购相…

ResNet网络的改进版:ResNeXt

之前的文章讲过ResNet网络的基本架构,其本质就是让网络的学习目的从学习转为学习,也就是学习输入和输出之间的残差信息,从而缓解了梯度消失和网络退化问题。 本文讲下ResNet网络的改进版:ResNeXt。 架构 下面是ResNet和ResNeXt的架…

9.2、面向对象高级特性(类方法和静态方法、property类属性、单例模式)

文章目录类方法和静态方法property类属性单例模式基于装饰器实现使用_ _ new _ _方法实现面向对象总结类方法和静态方法 类里面定义的方法称为实例方法,python解释器会自动将对象(或实例)传入方法【pycharm中会自动将self传入,se…

【机器学习】支持向量机【下】软间隔与核函数

有任何的书写错误、排版错误、概念错误等,希望大家包含指正。 在阅读本篇之前建议先学习: 【机器学习】拉格朗日对偶性 【机器学习】核函数 由于字数限制,分成两篇博客。 【机器学习】支持向量机【上】硬间隔 【机器学习】支持向量机【下】…

研发效能工程实践-利用Superset快速打造大数据BI平台

大数据BI平台自研之殇 随着互联网发展,现在随便哪个公司都手握大量数据。如何利用这些数据为公司商业带来价值,触使各个公司投入大量人力财力去做商业智能。 早期的BI可能就是公司Leader叫开发小哥写几句SQL导出数据,然后导入到Excel里绘制几…

echarts:nuxt项目使用echarts

一、项目环境 nuxt 2.X vue2.X vuex webpack 二、安装 yarn add echarts 三、使用 3.1、plugins目录下创建echarts.js import Vue from vue import * as echarts from echarts // 引入echarts Vue.prototype.$echarts echarts // 引入组件(将echarts注册为全…

认证服务-----技术点及亮点

大技术 Nacos做注册中心 把新建的微服务注册到Nacos上去 两个步骤 在配置文件中配置应用名称、nacos的发现注册ip地址,端口号在启动类上用EnableDiscoveryClient注解开启注册功能 使用Redis存验证码信息 加入依赖配置地址和端口号即可 直接注入StringRedisTempla…

HTML静态网页作业——关于我的家乡介绍安庆景点

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法,如盒子的嵌套、浮动、margin、border、background等属性的使用,外部大盒子设定居中,内部左中右布局,下方横向浮动排列,大学学习的前端知识点和布局方式都有…

Armbian搭建本地Gitea服务器

Armbian搭建本地Gitea服务器 1 安装Docker Docker 是一个用于开发、发布和运行应用程序的开放平台。 Docker 是一个开源的应用容器引擎,Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上&…

R语言中的prophet预测时间序列数据模型

本文 将针对R进行的几次建模练习的结果,以魁北克数据为依据,分为13年的训练和1年的测试。prophet与基本线性模型(lm),一般加性模型(gam)和随机森林(randomForest)进行了比…

ES6:ES6 的新增语法

什么是 ES6 ? ES 的全称是 ECMAScript , 它是由 ECMA 国际标准化组织,制定的一项脚本语言的标准化规范。 年份 版本 2015年6月 ES2015 2016年6月 ES2016 2017年6月 ES2017 2018年6月 ES2018 … … ES6 实际上是一个泛指,泛指 ES2015 及后续的版本。 …

基于最大熵图像插值Maximum Entropy插值算法的图像超分辨重构研究-附Matlab代码

⭕⭕ 目 录 ⭕⭕✳️ 一、引言✳️ 二、图像复原基本原理✳️ 三、最大熵图像插值原理✳️ 四、实验验证✳️ 五、参考文献✳️ 六、Matlab程序获取与验证✳️ 一、引言 图像是一种表达信息的形式,其中,数字图像反馈的信息更加丰富。 在获取图像的过程中…