考虑以下简单的 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 = 0
和 thread2::a = 0
?
我无法从 JMM 的角度证明它不可能发生,所以我开始分析 x86-64
的编译代码。
这是编译器对 thread1
和 thread2
方法的最终结果(与 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 = 0
和thread2::a = 0
是可能的。
最佳答案
是的,您的分析看起来正确。这是 StoreLoad 试金石,只有 一个 面具有 StoreLoad 屏障(如 C++ std::atomic
iwth memory_order_seq_cst
或 Java volatile
)。两者都需要关闭这种可能性。见 Jeff Preshing 的 Memory Reordering Caught in the Act有关双方都没有这种障碍的情况的详细信息。
a=1
与 b=this.b
的
StoreLoad 重新排序允许
的有效顺序 thread1 thread2
b=this.b // reads 0
b=1
a=this.a // reads 0
a=1
(这种困惑的名称是为什么示例和重新排序试金石测试选择像 r0
和 r1
这样的名称作为“寄存器”来讨论线程的加载结果是正常的观察到,与共享变量的名称绝对不同,这使得语句的含义与上下文相关,并且在重新排序图中查看和思考很痛苦。)
So thread1's Store can still be forwarded after the Load and therefore
thread1::b = 0
andthread2::a = 0
is possible.
您的意思似乎是“重新排序”,而不是转发。内存排序上下文中的“转发”意味着存储到加载转发(其中负载在全局可见之前从存储缓冲区中提取数据,因此它会立即看到自己的存储,相对于其他事物以不同的顺序比其他线程会)。但是您的两个线程都没有重新加载自己的商店,所以这不会发生。
x86 的内存模型基本上是程序顺序 + 带有存储到加载转发的存储缓冲区,因此 StoreLoad 重新排序是唯一可能发生的类型。
所以是的,这是最接近排除 ra=rb=0
的可能性,同时仍然留有一个窗口让其发生。在强排序 ISA (x86) 上运行,一侧有屏障。
当您在每个线程启动时只进行一次测试时,也不太可能观察到;毫不奇怪,这些执行需要 30 分钟才能在内核之间以足够接近的时间同时发生以观察到这一点。 (更快的测试不是微不足道的,就像第三个线程在测试之间重置事物并唤醒其他两个线程? 但是做一些事情让两个线程更有可能同时到达这个代码可能会有很大帮助,比如让它们都旋转等待同一个变量,所以它们可能会在彼此的一百个周期内醒来。 )
关于Java volatile 内存排序及其在 x86-64 上的编译,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73603561/