• 指令重排以及案例


    基本概念

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

    static int i;
    static int j;
    // 在某个线程内执行如下赋值操作
    i = ...; 
    j = ...;
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    j = ...
    i = ... 
    
    • 1
    • 2

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

    指令重排序优化原理

    事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    指令重排案例

    简单案例

    package cn.knightzz.instructions;
    
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * @author 王天赐
     * @title: TestDemo01
     * @projectName hm-juc-codes
     * @description: 诡异的情况
     * @website http://knightzz.cn/
     * @github https://github.com/knightzz1998
     * @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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

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

    • 线程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;
    
    • 1
    • 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
    
    • 1
    • 2
    • 3

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

    /*
     * 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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

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

    image-20220803080532638

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

    image-20220803081451143

    运行结果

    image-20220803124444485

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

    解决办法

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

    volatile boolean ready = false;
    
    • 1
  • 相关阅读:
    项目管理工具Maven(基础篇)
    React中如何在事件处理的时候传参(详解)
    Kafka 搭建过程
    工业 web4.0 的 UI 卓越非凡
    推荐一个分布式单点登录框架XXL-SSO!
    算法记录--好多内容也是借鉴大神的
    Spring Cloud学习:二【详细】
    前端技能树,面试复习第 45 天—— Vue 基础 | 模版编译原理 | mixin | use 原理 | 源码解析
    【基于MBD开发模式的matlab持续集成(一)】
    第四种Web语言:WebAssembly
  • 原文地址:https://blog.csdn.net/weixin_40040107/article/details/126138442