写点什么

我的代码出现幻觉?说好的 a = 1; x = b,怎么成了 x = b; a = 1?

作者:milanyangbo
  • 2025-07-28
    广东
  • 本文字数:1698 字

    阅读完需:约 6 分钟

我的代码出现幻觉?说好的a = 1; x = b,怎么成了x = b; a = 1?

有序性:代码执行的幻觉


前面讲到通过缓存一致性协议,来保障共享变量的可见性。那么是否还有其他情况,导致对共享变量操作不符合预期结果。可以看下面的代码:


private int a, b;private int x, y;
public void test() { Thread t1 = new Thread(() -> { x = b; a = 1; });
Thread t2 = new Thread(() -> { y = a; b = 2; }); // ...start启动线程,join等待线程 assert x == 2; assert y == 1;}
复制代码


假设将线程 t1 的代码块从 a = 1; x = b;改成 x = b; a = 1; 。将线程 t2 的代码块从 b = 2; y = a;改成 y = a; b = 2;。对于线程 t1 和 t2 自己来说,代码的重排序,不会影响当前线程执行。但是在多线程并发执行下,会出现如下情况:1)假设处理器 A 先将变量 b = 0 赋值给 x,再将变量 a 赋值 1。处理器 B 先将变量 a = 0 赋值给 y,再将变量 b 赋值 2。那么这时结果是:x 等于 0,y 等于 0。可见代码的重排序也会影响到程序最终结果。重排序是一种被编译器和处理器采用的优化策略,以便更有效地利用处理器资源,减少指令的执行延迟,以及提高并行指令的数量。在编译阶段,编译器会进行静态重排序。例如,编译器可能会将计算密集型的指令移动到 I/O 操作之前,以便在等待 I/O 完成时,处理器可以执行其他的计算任务。在运行阶段,现代处理器会进行动态重排序,也被称为指令重排序。例如,当一个指令需要等待数据从内存加载时,处理器可能会先执行其他没有数据依赖的指令,从而避免处理器空闲。



重排序需要遵守两点。1)数据依赖性:如果两个操作之间存在数据依赖,那么编译器和处理器不能调整它们的顺序。


// 写后读a = 1;b = a;// 写后写a = 1;a = 2;// 读后写a = b;b = 1;
复制代码


上面 3 种情况,编译器和处理器不能调整它们的顺序,否则将会造成程序语义的改变。2)as-if-serial 语义:即给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。


a = 1;b = 2;c = a * b;
复制代码


如上对变量 a 的赋值和对变量 b 的赋值,不存在数据依赖关系。因此对变量 a 和 b 重排序不会影响变量 c 的结果。但数据依赖性和 as-if-serial 语义只保证单个处理器中执行的指令序列和单个线程中执行的操作,并不考虑多核处理器和多线程之间的数据依赖情况。因此在多线程程序中,对存在数据依赖的操作重排序,可能会改变程序的执行结果。因此要避免程序的错误的执行,便是需要禁止这种编译和处理器优化导致的重排序。这种解决重排序问题的机制,叫做内存屏障。内存屏障也被称为内存栅栏或内存栅障,是一种用于处理多处理器编程中的同步问题的计算机指令。它的主要作用是防止某些内存操作的重排序。以日常接触的 X86_64 架构来说,内存操作指令如读读(LoadLoad)、读写(LoadStore)以及写写(StoreStore)内存屏障是空操作(no-op),只有写读(StoreLoad)内存屏障会被替换成具体指令。在 Java 语言中,内存屏障通过 volatile 关键字实现,禁止被它修饰的变量发生指令重排序操作:1)不允许 volatile 字段写操作之前的内存访问被重排序至其之后。2)不允许 volatile 字段读操作之后的内存访问被重排序至其之前。


//  变量a,b通过volatile修饰private volatile int a, b; private int x, y;
public void test() { Thread t1 = new Thread(() -> { a = 1; // 编译器插入storeload内存屏障指令 // 1)禁止代码和指令重排序 // 2)强制刷新变量a的最新值到内存 x = b; // 1)强制从内存中读取变量b的最新值 });
Thread t2 = new Thread(() -> { b = 2; // 编译器插入storeload内存屏障指令 // 1)禁止代码和指令重排序 // 2)强制刷新变量b的最新值到内存 y = a; // 1)强制从内存中读取变量a的最新值 }); // ...start启动线程,join等待线程 assert x == 2; assert y == 1;}
复制代码


可以看到通过 volatile 修饰的变量通过 LOCK 指令和内存屏障,实现共享变量的可见性和避免代码和指令的重排序,最终保障了程序在多线程情况下的正常执行。


未完待续


很高兴与你相遇!如果你喜欢本文内容,记得关注哦!!!

发布于: 刚刚阅读数: 4
用户头像

milanyangbo

关注

让世界知道我的存在 2018-03-05 加入

技术/人文, 互联网

评论

发布
暂无评论
我的代码出现幻觉?说好的a = 1; x = b,怎么成了x = b; a = 1?_并发编程_milanyangbo_InfoQ写作社区