likes
comments
collection
share

volatile 线程的有序性

作者站长头像
站长
· 阅读数 47

单线程串行指令执行情况

单线程中执行指令是按顺序去执行的,也就是说每条完整的指令都会这样去执行。 volatile 线程的有序性

指令排序优化

多核CPU可以做到:多级指令流水线型,支持同时执行多条指令(并行),类似这样:

volatile 线程的有序性

在蓝底中就表示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;
    }
}

测试结果:

volatile 线程的有序性

在测试结果中出现了: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
评论
请登录