volatile 线程的有序性
单线程串行指令执行情况
单线程中执行指令是按顺序去执行的,也就是说每条完整的指令都会这样去执行。
指令排序优化
多核CPU可以做到:多级指令流水线型,支持同时执行多条指令(并行),类似这样:
在蓝底中就表示CPU可以在同时执行多条指令,在JVM中针对这种多条指令执行可能会存在重排优化:
int a = 10; // 指令1
int b = 20; // 指令2
它可能会上下颠倒顺序,这两条指令是无依赖性的,所以并不会出现什么异常。
接下来我们看一个可能会出现异常的指令排序例子,这个例子需要借助:JCStress工具。
JCStress工具
JCStress是OpenJDK中的一个高并发测试工具,它可以帮助我们研究在高并发场景下JVM,类库以及硬件等状况。
依赖
<dependencies>
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>0.3</version>
</dependency>
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-samples</artifactId>
<version>0.3</version>
</dependency>
</dependencies>
测试 线程有序性的错误
@JCStressTest // 标记此类为一个并发测试类
@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "错误结果") // 描述测试结果
@Outcome(id = {"-1", "5"}, expect = Expect.ACCEPTABLE, desc = "正常结果") // 描述测试结果
@State //标记此类是有状态的
public class TestInstructionReorder {
private boolean flag;
private int x;
public TestInstructionReorder() {}
@Actor
public void actor1(I_Result r) {
if (flag) {
r.r1 = x;
} else {
r.r1 = -1;
}
}
@Actor
public void actor2(I_Result r) {
this.x = 5;
flag = true;
}
}
测试结果:
在测试结果中出现了:0,这个错误结果,出现0的原因就是因为指令重排导致的:
this.x = 5;
flag = true;
会变成:
flag = true;
this.x = 5;
在下面代码中,会先判断flag,而此时x还未赋值是0,所以会导致出现的结果为0
if (flag) {
r.r1 = x;
} else {
r.r1 = -1;
}
解决方法 - volatile
注意:无法解决多线程之间的指令交错,只能保证本线程的有序性。
使用volatile
关键字来修饰变量,原因是:volatile
底层是使用一种内存屏障技术。
private volatile boolean flag ;
内存屏障技术:
- 写屏障:对volatile变量进行修改(写指令)会加入写屏障
- 读屏障:对volatile变量进行读取(读指令)会加入读屏障
写屏障
保证在该屏障之前,对共享变量的改动都同步到主存中;而且之前的指令不会进行重排。
这里的 flag 和 x 都是共享变量,他们两个都会被同步到主存中。
private volatile boolean flag ; // 对flag 加上 volatile
@Actor
public void actor2(I_Result r) {
this.x = 5;
flag = true;
// 加入写屏障
}
读屏障
保证在该屏障之后,对共享变量的读取,加载的都是主存中最新数据;而且之后的指令不会进行重排。
@Actor
public void actor1(I_Result r) {
// 加入读屏障
if (flag) {
r.r1 = x;
} else {
r.r1 = -1;
}
}
转载自:https://juejin.cn/post/7240023332267524153