基本概念

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; 
j = ...;

比如 : 调整为 下面的 , 最终的结果也不会发生变化

j = ...
i = ... 

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。

指令重排序优化原理

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令 还可以再划分成一个个更小的阶段

例如,每条指令都可以分为:

  1. 取指令
  2. 指令译码
  3. 执行指令
  4. 内存访问
  5. 数据写回

这 5 个阶段

术语参考:

instruction fetch (IF)

instruction decode (ID)

execute (EX)

memory access (MEM)

register write back (WB)

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序组合来实现指令级并行,这一技术在 80’s 中 叶到 90’s 中叶占据了计算架构的重要地位。

分阶段,分工是提升效率的关键! , 指令重排的前提是,重排指令不能影响结果,例如

// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2

指令重排案例

简单案例

package cn.knightzz.instructions;

import lombok.extern.slf4j.Slf4j;

/**
 * @author 王天赐
 * @title: TestDemo01
 * @projectName hm-juc-codes
 * @description: 诡异的情况
 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a>
 * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a>
 * @create: 2022-08-03 07:07
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestDemo01")
public class TestDemo01 {

    boolean ready = false;
    int num = 0;

    public void actor01(I_Result r) {
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    public void actor02() {
        num = 2;
        ready = true;
    }

    public static void main(String[] args) throws InterruptedException {

        I_Result r = new I_Result();
        TestDemo01 demo01 = new TestDemo01();

        Thread t1 = new Thread(() -> {
            demo01.actor01(r);
            demo01.actor02();
        }, "t1");

        Thread t2 = new Thread(() -> {
            demo01.actor01(r);
            demo01.actor02();
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("r.r1 = " + r.r1);
    }

}

@SuppressWarnings("all")
class I_Result {
    int r1 = 0;
}

以上的代码可能会出现的情况:

  • 线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  • 线程2 先执行 num=2,但没来得及执行ready = true,线程1执行,还是进入else分支,结果为1
  • 线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

结果为0的情况 :

线程2开始执行, 由于 num = 2; ready = true 二者指令上没有依赖, 所以可以重排, JVM会对指令的顺序进行重新排序 :

ready = true;
num = 2;

执行在将 true 赋值给 ready 后, 此时 num = 2 还未执行

此时 线程上下文切换, if ready = ture , r.r1 = 0 + 0 最终的结果就是0

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:

借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress

创建项目 :

image-20220803075442466

GroupId : org.openjdk.jcstress
ArtifactId : jcstress-java-test-archetype
Version : 0.5

生成对应的项目后, 我们就可以添加相应的代码

/*
 * Copyright (c) 2017, Red Hat Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 *  * Neither the name of Oracle nor the names of its contributors may be used
 *    to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package cn.knightzz;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;
import org.openjdk.jcstress.infra.results.I_Result;

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

    int num = 0;
    volatile boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        // 出现指令重排
        num = 2;
        ready = true;
    }

}

然后使用mvn对代码进行打包

image-20220803080532638

执行生成的 jar 包 java -jar jcstress.jar -v

image-20220803081451143

运行结果

image-20220803124444485

可以看到上面的结果, 指令重排出现的概率很低, 我是没试出来

解决办法

解决办法 : 使用 volatile 修饰对应的变量 可以禁用指令重排

volatile boolean ready = false;