16

JMM 中的因果关系似乎是其中最令人困惑的部分。我有几个关于 JMM 因果关系的问题,以及并发程序中允许的行为。

据我了解,当前的 JMM 始终禁止因果循环。(我对吗?)

现在,根据JSR-133文档,第 24 页,图 16,我们有一个示例,其中:

最初x = y = 0

线程 1:

r3 = x;
if (r3 == 0)
    x = 42;
r1 = x;
y = r1;

线程 2:

r2 = y;
x = r2;

直觉上,r1 = r2 = r3 = 42似乎是不可能的。但是,它不仅被提及为可能,而且在 JMM 中也是“允许的”。

对于这种可能性,我无法理解的文件中的解释是:

编译器可以确定唯一分配给的值x是 0 和 42。据此,编译器可以推断出,在我们执行 的点,我们r1 = x刚刚执行了 42 的写入 x,或者我们刚刚阅读x并看到值 42。在任何一种情况下,读取值 42 都是合法的x。然后它可以更改r1 = xr1 = 42; 这将允许更早y = r1地转换y = 42和执行,从而导致有问题的行为。在这种情况下,首先提交写入y

我的问题是,究竟是什么样的编译器优化?(我对编译器一无所知。)由于 42 只是有条件地编写,当if满足该语句时,编译器如何决定编写x

其次,即使编译器进行了这种推测性的优化,并且提交y = 42然后最终使r3 = 42,这是否违反了因果循环,因为现在已经没有因果关系了?

事实上,在同一文档(第 15 页,图 7)中有一个示例,其中提到了类似的因果循环是不可接受的。

那么这个执行顺序为什么在 JMM 中是合法的呢?

4

3 回答 3

6

如前所述,唯一写入的值x是 0 和 42。线程 1:

r3 = x; // here we read either 0 or 42
if (r3 == 0)
  x = 42;  
// at this point x is definitely 42
r1 = x;

因此,JIT 编译器可以重写r1 = xr1 = 42,甚至更进一步y = 42。关键是,线程 1 将始终无条件地将 42 写入y. 该r3变量实际上是多余的,可以从机器代码中完全消除。所以示例中的代码只给出了从xto的因果箭头的外观y,但详细分析表明实际上没有因果关系。y令人惊讶的结果是可以提前提交写入。

关于优化的一般说明:我认为您熟悉从主内存读取所涉及的性能损失。这就是为什么 JIT 编译器一心想要尽可能拒绝这样做的原因,在这个例子中,事实证明它实际上不需要读取x就知道要写入什么y

关于符号的一般说明:r1, r2,r3局部变量(它们可以在堆栈上或 CPU 寄存器中);x,y共享变量(它们在主存储器中)。如果不考虑这一点,这些示例将毫无意义。

于 2012-11-07T15:02:28.063 回答
3

编译器可以执行一些分析和优化,并以 Thread1 的以下代码结束:

y=42; // step 1
r3=x; // step 2
x=42; // step 3

对于单线程执行,这段代码等同于原始代码,因此是合法的。然后,如果 Thread2 的代码在 step 1 和 step2 之间执行(这很有可能),那么 r3 也被分配 42。

此代码示例的整个想法是演示正确同步的需要。

于 2012-11-07T15:06:25.343 回答
1

没有在javac很大程度上优化代码是毫无价值的。JIT 优化了代码,但对重新排序代码相当保守。CPU 可以重新排序执行,并且在很小的程度上执行此操作。

强制 CPU 不进行指令级优化是相当昂贵的,例如它可以减慢 10 倍或更多。AFAIK,Java 设计者想要指定在大多数 CPU 上有效工作所需的最低保证。

于 2012-11-07T15:03:35.973 回答