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

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

文章目录

总结

一、指令重排序规范

二、指令重排序示例

总结

Java 并发的 3 33 特性 :


原子性 : 每个操作都是 不可拆分的原子操作 ; 在线程中进行 a++ 就不是原子操作 , 该操作分为 3 33 个步骤 , 首先从主内存中读取 a 变量 , 然后进行自增操作 , 最后在将自增后的值写回主内存中 ;

可见性 : 多个线程 访问同一个变量 , 该变量一旦被 某个线程修改 , 这些线程必须可以 立刻看到被修改的值 ;

有序性 : 程序按照 代码先后顺序 执行 ;

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


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


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






一、指令重排序规范


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



JVM 指令重排遵循规范 :


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


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

x = 0;
y = 1;


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

x = 0;
y = x;


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);
            }*/
        }
    }
}


执行结果 :

image.png




两个线程的线程调度 :


协同式调度 : 线程执行时间 由 线程 决定 ;

抢占式调度 : 线程执行事件 由 系统 决定 ;

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


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

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


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


目录
相关文章
|
7月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
248 0
|
9月前
|
设计模式 存储 SQL
【Java并发】【volatile】适合初学者体质的volatile
当你阅读dalao的框架源码的时候,你是否会见到这样一个关键字 - - - volatie,诶,你是否会好奇,为什么要加它?加了它有什么作用?
245 14
【Java并发】【volatile】适合初学者体质的volatile
|
9月前
|
存储 缓存 安全
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是写出高端的CRUD应用。2025年,我正在沉淀自己,博客更新速度也在加快。在这里,我会分享关于Java并发编程的深入理解,尤其是volatile关键字的底层原理。 本文将带你深入了解Java内存模型(JMM),解释volatile如何通过内存屏障和缓存一致性协议确保可见性和有序性,同时探讨其局限性及优化方案。欢迎订阅专栏《在2B工作中寻求并发是否搞错了什么》,一起探索并发编程的奥秘! 关注我,点赞、收藏、评论,跟上更新节奏,让我们共同进步!
394 8
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
|
10月前
|
缓存 安全 Java
Volatile关键字与Java原子性的迷宫之旅
通过合理使用 `volatile`和原子操作,可以在提升程序性能的同时,确保程序的正确性和线程安全性。希望本文能帮助您更好地理解和应用这些并发编程中的关键概念。
275 21
|
9月前
|
Java C语言
课时8:Java程序基本概念(标识符与关键字)
课时8介绍Java程序中的标识符与关键字。标识符由字母、数字、下划线和美元符号组成,不能以数字开头且不能使用Java保留字。建议使用有意义的命名,如student_name、age。关键字是特殊标记,如蓝色字体所示。未使用的关键字有goto、const;特殊单词null、true、false不算关键字。JDK1.4后新增assert,JDK1.5后新增enum。
193 4
|
8月前
|
存储 安全 Java
深入理解 Java 中的 instanceof 关键字
本文深入解析了 Java 中的 `instanceof` 关键字,探讨其在类型判断中的作用。作为二元操作符,`instanceof` 可用于检查对象是否为某类实例或实现特定接口,避免类型转换异常 (`ClassCastException`)。文章通过多态性下的类型判断、安全类型转换、接口实现检测及集合元素类型判定等实际应用场景,展示了 `instanceof` 的强大功能。掌握该关键字可提高代码健壮性,确保运行时类型安全。
597 0
|
12月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
337 5
Java 并发编程——volatile 关键字解析
|
11月前
|
Java 编译器 开发者
Java中的this关键字详解:深入理解与应用
本文深入解析了Java中`this`关键字的多种用法
1747 9
|
11月前
|
JSON Java 数据挖掘
利用 Java 代码获取淘宝关键字 API 接口
在数字化商业时代,精准把握市场动态与消费者需求是企业成功的关键。淘宝作为中国最大的电商平台之一,其海量数据中蕴含丰富的商业洞察。本文介绍如何通过Java代码高效、合规地获取淘宝关键字API接口数据,帮助商家优化产品布局、制定营销策略。主要内容包括: 1. **淘宝关键字API的价值**:洞察用户需求、优化产品标题与详情、制定营销策略。 2. **获取API接口的步骤**:注册账号、申请权限、搭建Java开发环境、编写调用代码、解析响应数据。 3. **注意事项**:遵守法律法规与平台规则,处理API调用限制。 通过这些步骤,商家可以在激烈的市场竞争中脱颖而出。
|
12月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
298 7