信息发布→ 登录 注册 退出

Java指令重排在多线程环境下的解决方式

发布时间:2026-01-11

点击量:
目录
  • 一、序言
  • 二、问题复原
    • (一)关联变量
      • 1、结果预测
      • 2、指令重排
    • (二)new创建对象
      • 1、解析创建过程
      • 2、重排序过程分析
  • 三、应对指令重排
    • (一)AtomicReference原子类
      • (二)volatile关键字
      • 四、指令重排的理解
        • 1、指令重排广泛存在
          • 2、多线程环境指令重排
            • 3、synchronized锁与重排序无关

            一、序言

            指令重排在单线程环境下有利于提高程序的执行效率,不会对程序产生负面影响;在多线程环境下,指令重排会给程序带来意想不到的错误。

            本文对多线程指令重排问题进行复原,并针对指令重排给出相应的解决方案。

            二、问题复原

            (一)关联变量

            下面给出一个能够百分之百复原指令重排的例子。

            public class D {
                static Integer a;
                static Boolean flag;
                
                public static void writer() {
                    a = 1;
                    flag = true;
                }
                
                public static void reader() {
                    if (flag != null && flag) {
                        System.out.println(a);
                        a = 0;
                        flag = false;
                    }
                }
            }

            1、结果预测

            reader方法仅在flag变量为true时向控制台打印变量a的值。

            writer方法先执行变量a的赋值操作,后执行变量flag的赋值操作。

            如果按照上述分析逻辑,那么控制台打印的结果一定全为1。

            2、指令重排

            假如代码未发生指令重排,那么当flag变量为true时,变量a一定为1。

            上述代码中关于变量a和变量flag在两个方法类均存在指令重排的情况。

            public static void writer() {
                a = 1;
                flag = true;
            }

            通过观察日志输出,发现有大量的0输出。

            writer方法内部发生指令重排时,flag变量先完成赋值,此时假如当前线程发生中断,其它线程在调用reader方法,检测到flag变量为true,那么便打印变量a的值。此时控制台存在超出期望值的结果。

            (二)new创建对象

            使用关键字new创建对象时,因其非原子操作,故存在指令重排,指令重排在多线程环境下会带来负面影响。

            public class Singleton {
                private static UserModel instance;
                
                public static UserModel getInstance() {
                    if (instance == null) {
                        synchronized (Singleton.class) {
                            if (instance == null) {
                                instance = new UserModel(2, "B");
                            }
                        }
                    }
                    return instance;
                }
            }
            
            @Data
            @AllArgsConstructor
            class UserModel {
                private Integer userId;
                private String userName;
            }

            1、解析创建过程

            • 使用关键字new创建一个对象,大致分为一下过程:
            • 在栈空间创建引用地址
            • 以类文件为模版在堆空间对象分配内存
            • 成员变量初始化
            • 使用构造函数初始化
            • 将引用值赋值给左侧存储变量

            2、重排序过程分析

            针对上述示例,假设第一个线程进入synchronized代码块,并开始创建对象,由于重排序存在,正常的创建对象过程被打乱,可能会出现在栈空间创建引用地址后,将引用值赋值给左侧存储变量,随后因CPU调度时间片耗尽而产生中断的情况。

            后续线程在检测到instance变量不为空,则直接使用。因为单例对象并为实例化完成,直接使用会带来意想不到的结果。

            三、应对指令重排

            (一)AtomicReference原子类

            使用原子类将一组相关联的变量封装成一个对象,利用原子操作的特性,有效回避指令重排问题。

            @Data
            @NoArgsConstructor
            @AllArgsConstructor
            public class ValueModel {
                private Integer value;
                private Boolean flag;
            }

            原子类应该是解决多线程环境下指令重排的首选方案,不仅通俗易懂,而且线程间使用的非重量级互斥锁,效率相对较高。

            public class E {
                private static final AtomicReference<ValueModel> ar = new AtomicReference<>(new ValueModel());
                
                public static void writer() {
                    ar.set(new ValueModel(1, true));
                }
                
                public static void reader() {
                    ValueModel valueModel = ar.get();
                    if (valueModel.getFlag() != null && valueModel.getFlag()) {
                        System.out.println(valueModel.getValue());
                        ar.set(new ValueModel(0, false));
                    }
                }
            }

            当一组相关联的变量发生指令重排时,使用原子操作类是比较优的解法。

            (二)volatile关键字

            public class Singleton {
                private volatile static UserModel instance;
                
                public static UserModel getInstance() {
                    if (instance == null) {
                        synchronized (Singleton.class) {
                            if (instance == null) {
                                instance = new UserModel(2, "B");
                            }
                        }
                    }
                    return instance;
                }
            }
            
            @Data
            @AllArgsConstructor
            class UserModel {
                private Integer userId;
                private String userName;
            }

            四、指令重排的理解

            1、指令重排广泛存在

            指令重排不仅限于Java程序,实际上各种编译器均有指令重排的操作,从软件到CPU硬件都有。指令重排是对单线程执行的程序的一种性能优化,需要明确的是,指令重排在单线程环境下,不会改变顺序程序执行的预期结果。

            2、多线程环境指令重排

            上面讨论了两种典型多线程环境下指令重排,分析其带来负面影响,并分别提供了应对方式。

            • 对于关联变量,先封装成一个对象,然后使用原子类来操作
            • 对于new对象,使用volatile关键字修饰目标对象即可

            3、synchronized锁与重排序无关

            synchronized锁通过互斥锁,有序的保证线程访问特定的代码块。代码块内部的代码正常按照编译器执行的策略重排序。

            尽管synchronized锁能够回避多线程环境下重排序带来的不利影响,但是互斥锁带来的线程开销相对较大,不推荐使用。

            synchronized 块里的非原子操作依旧可能发生指令重排

            到此这篇关于Java多线程环境指令重排的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持。

            在线客服
            服务热线

            服务热线

            4008888355

            微信咨询
            二维码
            返回顶部
            ×二维码

            截屏,微信识别二维码

            打开微信

            微信号已复制,请打开微信添加咨询详情!