【Java 并发编程】线程指令重排序问题 ( 指令重排序规范 | volatile 关键字禁止指令重排序 )

举报
韩曙亮 发表于 2022/01/13 00:54:33 2022/01/13
【摘要】 文章目录 总结一、指令重排序规范二、指令重排序示例 总结 Java 并发的 3 ...

总结

Java 并发的 3 3 3 特性 :

  • 原子性 : 每个操作都是 不可拆分的原子操作 ; 在线程中进行 a++ 就不是原子操作 , 该操作分为 3 3 3 个步骤 , 首先从主内存中读取 a 变量 , 然后进行自增操作 , 最后在将自增后的值写回主内存中 ;
  • 可见性 : 多个线程 访问同一个变量 , 该变量一旦被 某个线程修改 , 这些线程必须可以 立刻看到被修改的值 ;
  • 有序性 : 程序按照 代码先后顺序 执行 ;

使用 volatile 关键字只能保证 可见性 和 有序性 , 但是不能保证原子性 ;

volatile 可以激活线程共享变量的 " 缓存一致性协议 " ; 保证 可见性 ;

volatile 可以 禁止 JVM 的 " 指令重排 " ; 保证 有序性 ;





一、指令重排序规范



指令重排指的是 , 线程中如果两行代码 没有逻辑上的上下关系 , 可以对代码进行 重新排序 ;


JVM 指令重排遵循规范 :

as-if-serial 规范 : 单个线程中, 指令的重排 , 不能影响程序的执行结果 ;

  • 可以重排的情况 : 对于下面代码 , 两条指令顺序颠倒 , 执行结果相同 , 可以进行指令重排 ;
x = 0;
y = 1;

  
 
  • 1
  • 2
  • 不可以进行重排的情况 : 对于下面的代码 , 两条指令如果上下颠倒 , 结果不同 , 不可以进行指令重排 ;
x = 0;
y = x;

  
 
  • 1
  • 2

happens-before 规范 : 先行发生原则 ;





二、指令重排序示例



指令重排示例 :

public class Main {

    // 使用 volatile 关键字修饰变量可以禁止指令重排
    /*volatile static int x = 0;
    volatile static int y = 0;
    volatile static int a = 0;
    volatile static int b = 0;*/

    // 没有使用 volatile 关键字修饰, 会产生指令重排的情况
    static int x = 0;
    static int y = 0;
    static int a = 0;
    static int b = 0;

    /**
     * 多线程运行导致异常值出现, 是由于指令重排导致的
     * @param args
     */
    public static void main(String[] args) {
        // 设置一个非常大的遍历数
        //      指令重排出现过程很少见, 基数越大, 出现概率越高
        for (int i = 0; i < Integer.MAX_VALUE; i ++) {
            // 每次循环都初始化变量
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            // 在该线程中, 如果出现指令重排
            //      先执行 b = 1, 在执行 x = a
            new Thread(new Runnable() {
                @Override
                public void run() {
                    x = a;
                    b = 1;
                }
            }).start();

            // 如果出现指令重排
            //      先执行 a = 1, 在执行 y = b
            new Thread(new Runnable() {
                @Override
                public void run() {
                    y = b;
                    a = 1;
                }
            }).start();

            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            /*
                执行上述代码, 不是线程 1 先执行就是线程 2 先执行
                    如果线程 1 先执行, 则 x = 0, y = 1
                    如果线程 2 先执行, 则 x = 1, y = 0

                不可能出现 x = 1, y = 1 的情况
                    如果出现了, 则说明线程内部的执行顺序可能被颠倒了
                    出现了指令重排的情况
             */

            // 检查是否有异常值出现, 如果出现异常值, 退出方法
            if (x == 1 && y == 1) {
                System.out.println("出现异常值 x = 1, y = 1");
                return;
            } /*else {
                System.out.println("正常值 x = " + x + ", y = " + y);
            }*/
        }
    }
}

  
 
  • 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
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

执行结果 :

在这里插入图片描述


两个线程的线程调度 :

  • 协同式调度 : 线程执行时间 由 线程 决定 ;
  • 抢占式调度 : 线程执行事件 由 系统 决定 ;

上述示例中的线程调度方式是 " 抢占式调度 " , 谁先执行 由系统分配 , 这两个线程的执行顺序都是随机的 , 可能线程 1 先执行 , 也可能是线程 2 先执行 ;

如果线程 1 先执行, 则 x = 0, y = 1 ;
如果线程 2 先执行, 则 x = 1, y = 0 ;

根据代码分析 , 不可能出现 x = 1, y = 1 的情况 , 如果出现了, 则说明 线程内部的执行顺序可能被颠倒了 , 出现了指令重排的情况 ;

文章来源: hanshuliang.blog.csdn.net,作者:韩曙亮,版权归原作者所有,如需转载,请联系作者。

原文链接:hanshuliang.blog.csdn.net/article/details/120175581

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。