Java volatile 内存排序及其在 x86-64 上的编译

标签 java x86-64 volatile memory-barriers java-memory-model

考虑以下简单的 Java 应用程序:

public class Main {
    public int a;
    public volatile int b;

    public void thread1(){
        int b;
        a = 1;
        b = this.b;
    }

    public void thread2(){
        int a;
        b = 1;
        a = this.a;
    }

    public static void main(String[] args) throws Exception {
        Main m = new Main();
        while(true){
            m.a = 0;
            m.b = 0;
            Thread t1 = new Thread(() -> m.thread1());
            Thread t2 = new Thread(() -> m.thread2());
            t1.start();
            t2.start();
            t1.join();
            t2.join();
        }
    }
}

问题: 读入局部变量是否会导致 thread1::b = 0thread2::a = 0 ?

我无法从 JMM 的角度证明它不可能发生,所以我开始分析 x86-64 的编译代码。

这是编译器对 thread1thread2 方法的最终结果(与 while 循环无关的代码和 -XX:+PrintAssembly 为简单起见省略):

thread1:

  0x00007fb030dca235: movl    $0x1,0xc(%rsi)    ;*putfield a
  0x00007fb030dca23c: mov     0x10(%rsi),%esi   ;*getfield b

线程2:

  0x00007fb030dcc1b4: mov     $0x1,%edi
  0x00007fb030dcc1b9: mov     %edi,0x10(%rsi)
  0x00007fb030dcc1bc: lock addl $0x0,0xffffffffffffffc0(%rsp) ;*putfield b 
  0x00007fb030dcc1c2: mov     0xc(%rsi),%esi    ;*getfield a

所以我们这里得到的是 volatile 读取是免费完成的,volatile 写入需要 mfence(或 lock add) 之后。

所以thread1的Store在Load之后仍然可以转发,因此thread1::b = 0thread2::a = 0 是可能的。

最佳答案

是的,您的分析看起来正确。这是 StoreLoad 试金石,只有 一个 面具有 StoreLoad 屏障(如 C++ std::atomic iwth memory_order_seq_cst 或 Java volatile )。两者都需要关闭这种可能性。见 Jeff Preshing 的 Memory Reordering Caught in the Act有关双方都没有这种障碍的情况的详细信息。

a=1b=this.b

StoreLoad 重新排序允许

的有效顺序
   thread1        thread2
                  b=this.b        // reads 0
    b=1
    a=this.a                      // reads 0
                  a=1

(这种困惑的名称是为什么示例和重新排序试金石测试选择像 r0r1 这样的名称作为“寄存器”来讨论线程的加载结果是正常的观察到,与共享变量的名称绝对不同,这使得语句的含义与上下文相关,并且在重新排序图中查看和思考很痛苦。)

So thread1's Store can still be forwarded after the Load and therefore thread1::b = 0 and thread2::a = 0 is possible.

您的意思似乎是“重新排序”,而不是转发。内存排序上下文中的“转发”意味着存储到加载转发(其中负载在全局可见之前从存储缓冲区中提取数据,因此它会立即看到自己的存储,相对于其他事物以不同的顺序比其他线程会)。但是您的两个线程都没有重新加载自己的商店,所以这不会发生。

x86 的内存模型基本上是程序顺序 + 带有存储到加载转发的存储缓冲区,因此 StoreLoad 重新排序是唯一可能发生的类型。

所以是的,这是最接近排除 ra=rb=0 的可能性,同时仍然留有一个窗口让其发生。在强排序 ISA (x86) 上运行,一侧有屏障。

当您在每个线程启动时只进行一次测试时,也不太可能观察到;毫不奇怪,这些执行需要 30 分钟才能在内核之间以足够接近的时间同时发生以观察到这一点。 (更快的测试不是微不足道的,就像第三个线程在测试之间重置事物并唤醒其他两个线程? 但是做一些事情让两个线程更有可能同时到达这个代码可能会有很大帮助,比如让它们都旋转等待同一个变量,所以它们可能会在彼此的一百个周期内醒来。 )

关于Java volatile 内存排序及其在 x86-64 上的编译,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73603561/

相关文章:

java - java中是否需要添加 volatile 关键字来保证单例类的线程安全?

c# - .NET中双重检查锁定中对volatile修饰符的需求

java - 如何将句子分成两部分?

java - 在 java swing 中创建 X window(X11) 并获取它的 id

java - ScheduleAtFixedRate 方法 - 更改 TimeUnit 参数?

c - 堆栈中奇怪的指针位置

c++ - 如何将%rax中的double移到%ymm或%zmm上的特定qword位置? (卡比湖或更高版本)

assembly - x86-64 (AMD64) 架构中是否有默认操作数大小?

java - 如何修复此异常不匹配错误?

java - 从未分配为 null 的 volatile 变量是否可以包含 null?