阅读了更多的博客/文章等后,我对内存屏障前/后的加载/存储行为感到非常困惑。
以下是Doug Lea在他关于JMM的澄清文章中的两个引用,它们都非常直观:
- 当线程A写入volatile字段f时可见的任何内容,当线程B读取f时变得可见。
- 请注意,为了正确建立happens-before关系,必须让两个线程访问同一个volatile变量。并不是当线程A写入volatile字段f时可见的所有内容,在线程B读取volatile字段g之后就变得可见。
但是当我查看另一篇关于内存屏障的blog时,我发现了这些:
- 存储栅栏,x86上的“sfence”指令,强制在栅栏之前发生的所有存储指令都在栅栏之前发生,并将存储缓冲区刷新到发出指令的CPU的缓存中。
- 加载栅栏,x86上的“lfence”指令,强制在栅栏之后发生的所有加载指令都在栅栏之后发生,并等待该CPU上的加载缓冲区排空。
对我来说,Doug Lea的澄清比其他人更严格:基本上,这意味着如果加载栅栏和存储栅栏位于不同的监视器上,则无法保证数据一致性。但后者意味着即使栅栏位于不同的监视器上,也会保证数据一致性。我不确定我是否正确理解了这两个问题,也不确定哪一个是正确的。
考虑以下代码:
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");
}
}
}
假设我们有一个写线程TW1首先调用MemoryBarrier的write()方法,然后我们有两个读线程TR1和TR2调用MemoryBarrier的read1()和read2()方法。考虑这个程序在不保留顺序的CPU上运行(x86对于这种情况不会保留顺序),根据内存模型,在W01/W02之间将有一个StoreStore屏障(假设为SB1),以及在R11/R12和R21/R22之间将有两个LoadLoad屏障(假设为RB1和RB2)。
- 由于SB1和RB1都在同一台显示器i上,因此调用read1的线程TR1应始终在x上看到14,同时“Foo”总是被打印出来。
- SB1和RB2位于不同的显示器上,如果Doug Lea是正确的,那么线程TR2将不能保证在x上看到14,这意味着“Bar”可能会偶尔被打印出来。但如果内存屏障按照Martin Thompson在blog中描述的方式运行,存储屏障将把所有数据推送到主内存中,加载屏障将从主内存中拉取所有数据到缓存/缓冲区中,那么TR2也将被保证在x上看到14。
我不确定哪一个是正确的,或者两个都是正确的,但Martin Thompson描述的只是针对x86架构的。JMM不能保证对x的更改对TR2可见,但x86实现可以。
谢谢~
CountDownLatch
引入了额外的同步机制。因此,如果你使用CountDownLatch
来确保write
执行后read2
执行,那么read2
将始终打印 “Foo”。 - nosidsfence
不会将存储缓冲区刷新到缓存中。Peterson/Decker 同步需要mfence
。 - ninjalj