欢迎重视专栏【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、类库和硬件中并发支撑的正确性。详细运用可以参阅文章:www.cnblogs.com/wwjj4811/p/…
- 生成压测工程
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
这个关键字润饰变量,它可以保证有序性和可见性,但是无法保证原子性。假如今后遇到一些成员变量或者静态变量就要特别注意了,需要剖析并发状况下会有哪些问题。
假如本文对你有协助的话,请留下一个赞吧
本文正在参与「金石方案 . 分割6万现金大奖」