有序性问题

274 阅读5分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

有序性问题

接下来我们来学习并发编程中的第三个问题,有序性问题。那么咱们这个知识点的目标就是学习什么是有序性问题,我们学习的时候分成两步,第一步介绍有机性的概念。第二步通过一个案例来演示有序性问题。

有序性是指这个程序中代码的执行顺序。一般我们都会这么认为,就是我们编写代码的顺序就是程序最终的执行顺序,实际上并不一定是这样的。

那么为了提高我们程序的执行效率,那么 Java 在编译时和运行时,会对代码进行优化,会导致。程序最终的执行顺序不一定就是我们编写代码时顺序,这一点要特别注意,接下来呢我们就通过一个案例来演示有序性问题。

案例

这里面咱们会用到一个并发压测工具,wiki.openjdk.java.net/display/Cod…

说明:这个案例创建的是 maven 项目,相信你非常熟悉了,为了节省时间,具体创建步骤不做详细介绍。

修改 pom.xml 文件,添加依赖:

<dependency>
    <groupId>org.openjdk.jcstress</groupId>
    <artifactId>jcstress-core</artifactId>
    <version>0.7</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.openjdk.jcstress</groupId>
    <artifactId>jcstress-samples</artifactId>
    <version>0.7</version>
</dependency>

已经添加了这个依赖,添加了之后我们就来写相应的代码。

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class OrderingTest01 {

    int num = 0;
    boolean ready = false;

    // 线程 1 执行的代码
    @Actor
    public void actor1(IJ_Result r) {
        if (ready) {
            r.r1 = num + num;

        } else {
            r.r1 = 1;
        }
    }

    //线程 2 执行的代码
    @Actor
    public void actor2(IJ_Result r) {
        num = 2;
        ready = true;
    }

}

说明:这里有两个成员变量,一个 int 类型的 num 的变量 ,它的值是 0,一个 boolean 类型的变量,它的值是 false。有两个方法,actor1 这个方法先判断 redy 的值,然后做一些操作。actor2 方法对两个变量进行修改。另外咱们就要主要的是,方法中的参数有个参数叫 IJ_Result,IJ_Result个类不是我们自己写的,而是由 jcstress 并发压测工具自带的。

IJ_Result 这个类里面呢,它其实有一个成员变量,就是来保存一个 int 类型的结果。

public int r1;

除了IJ_Result 类之外,还有好多的类,它可以用来保存各种各样的结果,具体不多说。

我们在这两个方法的加了 @Actor 注解,@Actor 表示有多个线程来执行这两个方法。接着看这个类上面注解,@JCStressTest 表示用这个并发压测工具来对这个类的方法进行测试。@Outcome 有两个,它指的是对输出结果的处理。如果是1和 4 这两种结果表示是我们接受的,打印一个信息就 OK 没问题。如果我们程序最终方法中 IJ_Result 里面保存的结果是0,我们也认为他是可以接受,并且是感兴趣的,然后打一个danger。

我们先来分析一下 a 有几种运行的结果。实际上这两个方法有很多的线程来执行。为了好说明问题,我们就有一个方法一个线程可以的。

image.png

这里有这么几种情况:

第一种情况:是执行 actor1 方法的线程先执行,此时 ready 值是false,所以执行 else。此时会把 1 赋给这个对象里面的成分,最终结果就是 1。

第二种情况:我们假设执行 actor2 方法的线程先执行,执行完 actor2 方法后,这时 CPU 切到执行 actor1 方法的线程。刚才 ready 的值被变成 true,所以 if 里面的语句,然后 num 变成了 2,2 +2 = 4,所以这个结果是四。

第三种情况:我们假设执行 actor2 方法的线程先执行。执行到 num = 2,此时 CPU 切到另外一个执行 actor1 方法的线程,这时 ready 的值还是 false 所以还是执行了 else 语句,这个结果还是 1。

第四种情况:我们可能很难发现,这是由于 Java 在编译时和运行时的优化,它可能会对我们的代码进行一个重排序,比如说它可能会排成这样。

num = 2;
ready = true;

因为这两句代码没有什么因果关系,如果排成这样之后,那么咱们再来分析一下。假设执行 actor2 方法的的线程先执行,执行第一句 ready 的值为 true,此时 CPU 切到了切到执行 actor1 方法的线程进行执行,执行的时候 ready 的值为 true,然后进行 if 里面代码的执行,此时 num 并没有赋值,所以是 0,最终这个结果是 0。

这种结果的原因是不是就是因为两句代码被重新排序,真的有吗?现在来通过这个压测工具来测试一下这些结果有没有这个 0 的。

运行测试命令:

mvn clean instal

java - jar target/jcstress.jar

小结:有序性是指我们程序中代码的执行先后顺序,为了提高执行效率。Java 在编译期和运行期会做优化,这就会导致我们最终程序的执行顺序可能跟我们编写代码的顺序是不一样的。最后咱们就已经能够了解到并发编程中,存在这三个问题,可见性问题、原子性问题和有序性问题,这三个问题呢都会导致我们共享数据错乱,会出现线程安全问题。