Java 内存模型和并发

Java Memory Model and Concurrency

鉴于 x86 总存储顺序和 Java 内存模型中的发生前关系,我们知道编译器不保证指令的执行顺序。它可以根据需要重新排序,以提高性能。鉴于此,我们有:

所以我有两组问题,我试图确定可能的输出:

[x] == [y] == 0 // the address space of [x] and [y] are 0.

// Thread 1                         Thread 2
MOV [x] <- 1                        MOV [y] <- 1
MOV EAX <- [y]                      MOV EBX <- [x]

寄存器 EBXEAX 的可能值是什么?

int x = 0;
int y = 0;

// Thread 1                         Thread 2
x = 1;                              y = 1; 
r1 = y;                             r2 = x;

r1r2 的可能值是多少?

我们可以简单地标记语句如下:

A) [x] <- 1            C) [y] <- 1

B) EAX <- [y]           D) EBX <- [x]

我们知道A在B之前,C在D之前,所以只需将C和D插入AB中所有可能的排列即可:

CDAB
CADB
CABD
ACDB
ACBD
ABCD

并考虑每种可能性的含义,注意到大多数以 ACCA 开头,输出 (EAX,EBX)=(1,1) 因为赋值发生在 EAX 之前并且EBX 正在设置中。剩下的就是检查其他两种可能性。 CDAB 给出 (EAX,EBX)=(1,0)ABCD 给出 (EAX,EBX)=(0,1).

对于 Java 版本,您声明编译器不保证语句执行的顺序。那样的话,命令ABCD得到(0,0)、(1,0)、 (0,1), 和 (1,1).

32-bit integerJVM 保证是 atomic,所以这不是问题。

您有 2 个变量 x 和 y 在没有 synchronization 的线程之间共享。

  1. Thread1 变异 x 并读取 y。
  2. Thread2 对 y 进行变异并读取 x。

因此,thread1 可以看到旧值 y(1 或 0),而 thread2 可以看到旧值 x (1,0)。

这意味着您可以获得 (eax, ebx) 的所有四种可能组合: (0,0) (0,1) (1,0) (1,1)

x86 具有强序内存模型,但仍然允许 StoreLoad reordering.

Jeff Preshing 的博客 post:Memory Reordering Caught in the Act,恰好使用那对存储然后加载序列作为测试用例,以证明在真实硬件上确实可以观察到重新排序。他有源代码和一切。

请注意,每个线程都有自己的体系结构状态(包括所有寄存器)。所以thread1的EAX和thread2的EAX是不一样的。在 thread2 中使用 EBX 只是更容易谈论,与可能发生的 POV 没有任何区别。

无论如何,两个寄存器都可以以 0 结尾。这种情况很少发生,但它会发生,因为每个线程的存储都可以延迟(在存储缓冲区或其他任何地方),直到另一个线程的加载选择了一个值之后。如果这是合法的,那么 CPU 可以积极地使用预取数据来满足负载,并缓冲存储,这样它们就不会在退出时立即变得全局可见。 ("retire" 表示线程的体系结构状态(包括 EIP)运行 该指令已移至下一条指令,并且已提交效果。)

其他可能性,一旦尘埃落定,总是包括两个全局变量 1。每个线程的寄存器中所有 4 个可能的值 0 和 1 都是可能的,包括 1。他们有可能看到彼此的商店。我不确定这有多大可能;它可能需要一个线程在其存储之后但在其加载之前被中断。如果两个线程在同一个物理内核(超线程)上 运行,则 .


即使 xy 的存储未对齐并跨越缓存行,01 也是唯一可能的值。 (C 编译器输出和 JVM 会将变量对齐到它们的自然对齐方式,这不是问题,但是您可以在 asm 中做任何您想做的事情,所以我想我会提到它。)发生这种情况是因为两个值仅不同在最低有效字节中。

如果您将 32 位 -1 存储到跨越两个缓存行的 4 个字节,则另一个线程可以加载 0x00ffffff0xff0000000x0000ffff 的值或 0xffff0000,等等(取决于缓存行边界的位置),以及通常的 00xffffffff(又名 -1)。


回复:Java。我还没有阅读 Java 内存模型。其他答案说它甚至允许编译时重新排序(如 )。即使没有,如果没有完整的内存屏障,StoreLoad 重新排序也会发生。所以所有四个结果都是可能的

即使您的 JVM 在 x86 CPU 上 运行(而不是像 ARM 这样的弱顺序硬件)也是如此。

可能会阐明为什么 LFENCE/SFENCE 存在于 x86 上,即使它们在大多数情况下都是空操作。 (即当不使用 movnt 或弱排序内存区域(如 USWC 视频内存)时)。

或者,只需阅读 Jeff Preshing 的其他博客 post 即可了解有关内存排序的更多信息。我发现它真的 对我自己有帮助。