深刻理解JAVA并发中的有序性问题和解决之道
欢迎关注专栏【JAVA并发】
更多技术干活尽在个人公众号——JAVA旭阳
问题
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值
可能是多少呢?
- r1值等于4, 这个大家都能想到, CPU先执行了线程2,然后执行线程1
- r1值等于1,这个也容易理解,CPU先执行了线程1,然后执行线程2
- 那我如果说r1值有可能等于0,大家可能觉得离谱,不信的话,我们验证下。
压测验证结果
由于并发问题出现的概率比较低,我们可以使用openjdk
提供的jcstress
框架进行压测,就能够出现各种可能的情况。
jcstress:全名The Java Concurrency Stress tests,是一个实验工具和一套测试工具,用于帮助研究JVM、类库和硬件中并发支持的正确性。详细使用可以参考文章:https://www.cnblogs.com/wwjj4811/p/14310930.html
- 生成压测工程
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
生成的工程代码如下图:
- 填充测试内容
- 方法
actor1
是压测第一个线程干的活,将结果保存到I_Result
中。 - 方法
actor2
是压测第二个线程干的活 - 类前面的
@Outcome
注解用来展示验证结果,特别是id="0"
这个是我们感兴趣的结果
- 运行压测工程
mvn clean install
java -jar target/jcstress.jar
- 查看运行结果
运行结果如下图所示:
- 有4000多次出现了0的结果
- 大部分情况的结果还是1和4
你是不是还是很困惑,其实这就是并发执行的一些坑,我们下面来解释下原因。
原因分析
如果先要出现r1的值等于0
,那么有一个可能0+0=0
,那么也就是num=0
。
你可能想num怎么可能等于0,代码逻辑明明是先设置num=2
,然后才修改ready=true
, 最后才会走到num+num
的逻辑啊....
在并发的世界里,我们千万不要被固有的思维限制了,那是不是有可能num=2
和ready=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指令,形象点理解就是一个栅栏,拦在那里,无法跨越。
内存屏障分为写屏障和读屏障,有什么有呢?
- 保证可见性
- 写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中
- 读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
- 保证有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
回到前面的问题,如果对ready
加了volatile
以后,那么num=2就无法到后面去了,同样读取也是,如上图所示。
final底层也是通过内存屏障实现的,它与volatile一样。
- 对final变量的写指令加入写屏障。也就是类初始化的赋值的时候会加上写屏障。
- 对final变量的读指令加入读屏障。加载内存中final变量的最新值。
总结
JAVA并发中的有序性问题其实比较难理解,本文通过一个例子验证了并发情况下会出现有序性的问题,从而引发意想不到的结果。这个主要的原因是为了提高性能,指令会发生重排序导致的。为了解决这样的问题,我们可以使用volatile
这个关键字修饰变量,它能够保证有序性和可见性,但是无法保证原子性。如果以后遇到一些成员变量或者静态变量就要特别注意了,需要分析并发情况下会有哪些问题。
如果本文对你有帮助的话,请留下一个赞吧
深刻理解JAVA并发中的有序性问题和解决之道的更多相关文章
- 深刻理解Java中final的作用(一):从final的作用剖析String被设计成不可变类的深层原因
声明:本博客为原创博客,未经同意,不得转载!小伙伴们假设是在别的地方看到的话,建议还是来csdn上看吧(原文链接为http://blog.csdn.net/bettarwang/article/det ...
- 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.AQS框架简介 AQS诞生于Jdk1.5,在当时低效且功能单一的synchroni ...
- 深入理解Java虚拟机--中
深入理解Java虚拟机--中 第6章 类文件结构 6.2 无关性的基石 无关性的基石:有许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码(ByteCode),从而 ...
- 深入浅出Java并发中的CountDownLatch
1. CountDownLatch 正如每个Java文档所描述的那样,CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行.在Java并发中 ...
- 深入理解Java并发框架AQS系列(一):线程
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.概述 1.1.前言 重剑无锋,大巧不工 读j.u.c包下的源码,永远无法绕开的经典 ...
- 深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock) 深入 ...
- java并发中CountDownLatch的使用
文章目录 主线程等待子线程全都结束之后再开始运行 等待所有线程都准备好再一起执行 停止CountdownLatch的await java并发中CountDownLatch的使用 在java并发中,控制 ...
- java并发中ExecutorService的使用
文章目录 创建ExecutorService 为ExecutorService分配Tasks 关闭ExecutorService Future ScheduledExecutorService Exe ...
- java并发中的Synchronized关键词
文章目录 为什么要同步 Synchronized关键词 Synchronized Instance Methods Synchronized Static Methods Synchronized B ...
- java开发中遇到的问题及解决方法(持续更新)
摘自 http://blog.csdn.net/pony12/article/details/38456261 java开发中遇到的问题及解决方法(持续更新) 工作中,以C/C++开发为主,难免与其他 ...
随机推荐
- Elasticsearch:Smart Chinese Analysis plugin
Smart Chinese Analysis插件将Lucene的Smart Chinese分析模块集成到Elasticsearch中,用于分析中文或中英文混合文本. 支持的分析器在大型训练语料库上使用 ...
- Traefik知识点
Traefik 的各种 Providers Traefik 中的配置发现是通过下面的一些 providers 来实现的. providers 是现有的一些基础架构组件,可以是编排工具,容器引擎,云提供 ...
- 使用Prometheus和Grafana监控RabbitMQ集群 (使用RabbitMQ自带插件)
配置RabbitMQ集群 官方文档:https://www.rabbitmq.com/prometheus.html#quick-start 官方github地址:https://github.com ...
- Centos7安装redash
一.更改yum国内源: (1)cd /etc/yum.repos.d/ sudo yum install wget (2)备份:sudo mv /etc/yum.repos.d/CentOS-Base ...
- Java复制Word文档
Microsoft Word 提供了许多易于使用的文档操作工具,同时也提供了丰富的功能集供创建复杂的文档使用.在使用的时候,你可能需要复制一个文档里面的内容到另一个文档.本文介绍使用Spire.Doc ...
- Leetcode栈&队列
Leetcode栈&队列 232.用栈实现队列 题干: 思路: 栈是FILO,队列是FIFO,所以如果要用栈实现队列,目的就是要栈实现一个FIFO的特性. 具体实现方法可以理解为,准备两个栈, ...
- 自定义ListView下拉刷新上拉加载更多
自定义ListView下拉刷新上拉加载更多 自定义RecyclerView下拉刷新上拉加载更多 Listview现在用的很少了,基本都是使用Recycleview,但是不得不说Listview具有划时 ...
- [Android开发学iOS系列] iOS写UI的几种方式
[Android开发学iOS系列] iOS写UI的几种方式 作为一个现代化的平台, iOS的发展也经历了好几个时代. 本文讲讲iOS写UI的几种主要方式和各自的特点. iOS写UI的方式 在iOS中写 ...
- 4.MongoDB系列之索引(一)
1. 执行计划查看 db.getCollection('users').find({'username': 'shenjian'}).explain('executionStats') 结果查看,先大 ...
- vue-router(路由嵌套)
文章目录 1.项目结构 2.路由嵌套 3.界面(使用elementui) 4.效果展示 1.项目结构 2.路由嵌套 import Vue from 'vue' import Router from ' ...