在阅读了更多博客/文章等之后,我现在对内存屏障之前/之后的加载/存储行为感到困惑。
以下是Doug Lea在他关于JMM的一篇澄清文章中的两个引用,它们都是非常直接的:
但是当我看到关于记忆障碍的另一个blog时,我得到了这些:
对我而言,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");
}
}
}
假设我们有1个写线程TW1首先调用MemoryBarrier的write()方法,然后我们有2个读取器线程TR1和TR2调用MemoryBarrier的read1()和read2()方法。考虑这个程序在CPU上运行,它不保留排序(x86 DO保留这种情况的排序而不是这种情况),根据内存模型,在W01之间会有一个StoreStore屏障(让我们说SB1) / W02,以及R11 / R12和R21 / R22之间的2个LoadLoad屏障(让我们说RB1和RB2)。
我不确定哪一个是正确的,或者两者都是正确的,但Martin Thompson所描述的仅适用于x86架构。 JMM不保证对TR的可见变化对于TR2是可见的,但x86实现确实如此。
感谢〜
答案 0 :(得分:15)
<强> §17.4.4 Synchronization Order 强>
[..]写入易失性变量 v (§8.3.1.4)与 v 的所有后续读取同步 - 任何线程(根据同步顺序定义“后续”)。 [..]
具体机器的内存模型并不重要,因为Java编程语言的语义是用抽象机器定义的 - 独立于具体机器。 Java运行时环境的责任是以这样的方式执行代码,它符合 Java语言规范提供的保证。
关于实际问题:
read2
可以打印"Bar"
,因为read2
可以在write
之前执行。CountDownLatch
进行了额外的同步,以确保在 read2
之后执行了write
,那么方法read2
将永远不会打印"Bar"
,因为与CountDownLatch
的同步会删除x
上的数据竞赛。独立的易变量:
对volatile变量的写入是否与读取任何其他volatile变量同步是否有意义?
是的,这很有道理。如果两个线程需要相互交互,则它们通常必须使用相同的volatile
变量才能交换信息。另一方面,如果一个线程使用volatile变量而不需要与所有其他线程进行交互,我们不想为内存障碍支付费用。
实际上这很重要。让我们举个例子。以下类使用volatile成员变量:
class Int {
public volatile int value;
public Int(int value) { this.value = value; }
}
想象一下,这个类只在方法中本地使用。 JIT编译器可以轻松检测到该对象仅在此方法中使用(Escape analysis)。
public int deepThought() {
return new Int(42).value;
}
根据上述规则,JIT编译器可以删除volatile
读写的所有效果,因为volatile
变量无法从任何其他线程访问。
这种优化实际上存在于Java JIT编译器中:
答案 1 :(得分:1)
据我所知,问题实际上是关于易失性读/写及其发生前保证。说到那一部分,我只有一件事要添加到nosid的回答中:
在正常写入之前无法移动易失性写入,正常读取后无法移动易失性读取。这就是为什么read1()
和read2()
结果会像nosid写的那样。
谈到障碍 - 定义对我来说听起来不错,但可能让你感到困惑的一件事就是这些是事物/工具/方式/机制(称之为你喜欢的任何方式)来实现热点中JMM中描述的行为。使用Java时,您应该依赖JMM保证,而不是实现细节。