Java中内存屏障的行为
在阅读了更多的博客/文章等之后,我现在对内存屏障之前/之后的加载/存储行为感到非常困惑。
以下是Doug Lea在他关于JMM的一篇澄清文章中引用的2句话,这些话都非常直截了当:
- 当线程 A 写入易失性字段 f 时,线程 A 可见的任何内容在读取 f 时对线程 B 可见。
- 请注意,两个线程都必须访问相同的易失性变量,以便正确设置发生前关系。当线程 A 写入易失性字段 f 时,线程 A 在读取易失性字段 g 后,并不是线程 B 可以看到的所有内容都变得可见的情况并非如此。
但是当我查看另一个关于记忆障碍的博客时,我得到了这些:
- 存储屏障(x86 上的“sfence”指令)强制在屏障之前的所有存储指令在屏障之前发生,并将存储缓冲区刷新到发出该屏障的 CPU 的缓存中。
- 负载屏障(x86 上的“lfence”指令)强制屏障之后的所有加载指令在屏障之后发生,然后等待负载缓冲区耗尽该 CPU。
对我来说,Doug Lea的澄清比另一个更严格:基本上,这意味着如果负载屏障和商店屏障位于不同的监视器上,则数据一致性将无法保证。但后者意味着即使障碍位于不同的监视器上,数据一致性也会得到保证。我不确定我是否正确地理解了这2个,我也不确定其中哪个是正确的。
考虑以下代码:
public class MemoryBarrier {
volatile int i = 1, j = 2;
int x;
public void write() {
x = 14; //W01
i = 3; //W02
}
public void read1() {
if (i == 3) { //R11
if (x == 14) //R12
System.out.println("Foo");
else
System.out.println("Bar");
}
}
public void read2() {
if (j == 2) { //R21
if (x == 14) //R22
System.out.println("Foo");
else
System.out.println("Bar");
}
}
}
假设我们有 1 个写入线程 TW1 首先调用 MemoryBarrier 的 write() 方法,然后我们有 2 个读取器线程 TR1 和 TR2 调用 MemoryBarrier 的 read1() 和 read2() 方法。考虑这个程序在不保留排序的CPU上运行(x86 DO为这种情况保留排序),根据内存模型,W01 / W02之间将存在StoreStore屏障(假设SB1),以及R11 / R12和R21 / R22之间的2个LoadLoad屏障(假设RB1和RB2)。
- 由于 SB1 和 RB1 位于同一监视器 i 上,因此调用 read1 的线程 TR1 应始终在 x 上看到 14,并且始终打印“Foo”。
- SB1 和 RB2 位于不同的显示器上,如果 Doug Lea 是正确的,线程 TR2 将不能保证在 x 上看到 14,这意味着“Bar”可能会偶尔打印出来。但是,如果内存屏障像Martin Thompson在博客中描述的那样运行,则Store屏障会将所有数据推送到主内存,而Load barrier会将所有数据从主内存拉取到缓存/缓冲区,那么TR2也将保证在x上看到14。
我不确定哪一个是正确的,或者两者都是正确的,但Martin Thompson所描述的只是针对x86架构的。JMM 不保证对 x 的更改对 TR2 是可见的,但 x86 实现是可见的。
谢谢~