Java 内存模型和并发
Java Memory Model and Concurrency
鉴于 x86
总存储顺序和 Java 内存模型中的发生前关系,我们知道编译器不保证指令的执行顺序。它可以根据需要重新排序,以提高性能。鉴于此,我们有:
EAX
,EBX
是寄存器的名字
[x]
、[y]
是内存位置
r1
和r2
是局部变量的名字
x
、y
是所有线程都可以访问的共享变量。所有变量都是 32 位整数。
- 不,这不是作业问题。
所以我有两组问题,我试图确定可能的输出:
[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]
寄存器 EBX
和 EAX
的可能值是什么?
int x = 0;
int y = 0;
// Thread 1 Thread 2
x = 1; y = 1;
r1 = y; r2 = x;
r1
和 r2
的可能值是多少?
我们可以简单地标记语句如下:
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
并考虑每种可能性的含义,注意到大多数以 AC
或 CA
开头,输出 (EAX,EBX)=(1,1)
因为赋值发生在 EAX
之前并且EBX
正在设置中。剩下的就是检查其他两种可能性。 CDAB
给出 (EAX,EBX)=(1,0)
,ABCD
给出 (EAX,EBX)=(0,1)
.
对于 Java 版本,您声明编译器不保证语句执行的顺序。那样的话,命令A
、B
、C
、D
得到(0,0)、(1,0)、 (0,1), 和 (1,1).
写 32-bit integer
被 JVM
保证是 atomic
,所以这不是问题。
您有 2 个变量 x 和 y 在没有 synchronization
的线程之间共享。
Thread1
变异 x 并读取 y。
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
。他们有可能看到彼此的商店。我不确定这有多大可能;它可能需要一个线程在其存储之后但在其加载之前被中断。如果两个线程在同一个物理内核(超线程)上 运行,则 .
即使 x
和 y
的存储未对齐并跨越缓存行,0
和 1
也是唯一可能的值。 (C 编译器输出和 JVM 会将变量对齐到它们的自然对齐方式,这不是问题,但是您可以在 asm 中做任何您想做的事情,所以我想我会提到它。)发生这种情况是因为两个值仅不同在最低有效字节中。
如果您将 32 位 -1
存储到跨越两个缓存行的 4 个字节,则另一个线程可以加载 0x00ffffff
或 0xff000000
、0x0000ffff
的值或 0xffff0000
,等等(取决于缓存行边界的位置),以及通常的 0
或 0xffffffff
(又名 -1
)。
回复:Java。我还没有阅读 Java 内存模型。其他答案说它甚至允许编译时重新排序(如 )。即使没有,如果没有完整的内存屏障,StoreLoad 重新排序也会发生。所以所有四个结果都是可能的。
即使您的 JVM 在 x86 CPU 上 运行(而不是像 ARM 这样的弱顺序硬件)也是如此。
可能会阐明为什么 LFENCE/SFENCE 存在于 x86 上,即使它们在大多数情况下都是空操作。 (即当不使用 movnt
或弱排序内存区域(如 USWC 视频内存)时)。
或者,只需阅读 Jeff Preshing 的其他博客 post 即可了解有关内存排序的更多信息。我发现它真的 对我自己有帮助。
鉴于 x86
总存储顺序和 Java 内存模型中的发生前关系,我们知道编译器不保证指令的执行顺序。它可以根据需要重新排序,以提高性能。鉴于此,我们有:
EAX
,EBX
是寄存器的名字[x]
、[y]
是内存位置r1
和r2
是局部变量的名字x
、y
是所有线程都可以访问的共享变量。所有变量都是 32 位整数。- 不,这不是作业问题。
所以我有两组问题,我试图确定可能的输出:
[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]
寄存器 EBX
和 EAX
的可能值是什么?
int x = 0;
int y = 0;
// Thread 1 Thread 2
x = 1; y = 1;
r1 = y; r2 = x;
r1
和 r2
的可能值是多少?
我们可以简单地标记语句如下:
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
并考虑每种可能性的含义,注意到大多数以 AC
或 CA
开头,输出 (EAX,EBX)=(1,1)
因为赋值发生在 EAX
之前并且EBX
正在设置中。剩下的就是检查其他两种可能性。 CDAB
给出 (EAX,EBX)=(1,0)
,ABCD
给出 (EAX,EBX)=(0,1)
.
对于 Java 版本,您声明编译器不保证语句执行的顺序。那样的话,命令A
、B
、C
、D
得到(0,0)、(1,0)、 (0,1), 和 (1,1).
写 32-bit integer
被 JVM
保证是 atomic
,所以这不是问题。
您有 2 个变量 x 和 y 在没有 synchronization
的线程之间共享。
Thread1
变异 x 并读取 y。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
。他们有可能看到彼此的商店。我不确定这有多大可能;它可能需要一个线程在其存储之后但在其加载之前被中断。如果两个线程在同一个物理内核(超线程)上 运行,则
即使 x
和 y
的存储未对齐并跨越缓存行,0
和 1
也是唯一可能的值。 (C 编译器输出和 JVM 会将变量对齐到它们的自然对齐方式,这不是问题,但是您可以在 asm 中做任何您想做的事情,所以我想我会提到它。)发生这种情况是因为两个值仅不同在最低有效字节中。
如果您将 32 位 -1
存储到跨越两个缓存行的 4 个字节,则另一个线程可以加载 0x00ffffff
或 0xff000000
、0x0000ffff
的值或 0xffff0000
,等等(取决于缓存行边界的位置),以及通常的 0
或 0xffffffff
(又名 -1
)。
回复:Java。我还没有阅读 Java 内存模型。其他答案说它甚至允许编译时重新排序(如
即使您的 JVM 在 x86 CPU 上 运行(而不是像 ARM 这样的弱顺序硬件)也是如此。
movnt
或弱排序内存区域(如 USWC 视频内存)时)。
或者,只需阅读 Jeff Preshing 的其他博客 post 即可了解有关内存排序的更多信息。我发现它真的 对我自己有帮助。