16

JMM中的因果关系似乎是最令人困惑的部分。我有几个关于JMM因果关系的问题,并允许并发程序中的行为。为什么在Java内存模型中允许这种行为?

据我所知,目前的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.在任何一种情况下,它都会合法读取x查看价值 42.然后它可以将r1 = x更改为r1 = 42;这将允许y = r1转换为y = 42并且执行得更早,导致 有问题的行为。在这种情况下,首先写入y承诺 。

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

其次,即使编译器这个投机的优化,并承诺y = 42和 然后最后使得r3 = 42,是不是违反了因果循环的,因为没有原因,现在的效果区别剩下什么?

实际上,在同一文件(第15页,图7)中有一个例子,其中提到了一个类似的因果回路,因为这是不可接受的。

那么这个执行命令在JMM中怎么合法呢?

回答

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实际上是多余的,可以从机器代码中完全消除。因此,该示例中的代码仅给出了从xy的因果箭头的外观,但详细分析表明实际上没有因果关系。令人惊讶的后果是,写入y可以提前做出承诺。

关于优化的一般注意事项:我认为您熟悉从主内存读取时涉及的性能损失。这就是为什么JIT编译器倾向于尽可能拒绝这样做,在这个例子中,事实证明它实际上并不需要阅读x以便知道要写入y的内容。

上符号的一般注释:r1r2r3局部变量(它们可以是对堆或在CPU寄存器); x,y共享变量(这些都在主存中)。没有考虑到这些,这些例子就没有意义了。

1

javac没有对代码进行很大程度的优化。 JIT优化了代码,但对重新排序代码相当保守。 CPU可以重新排序执行,并且它可以很小程度地完成这个任务。

强制CPU不做指令级优化是相当昂贵的,例如,它可以减慢10倍或更多。 AFAIK,Java设计者希望指定所需的最低限度的保证,这些保证可以在大多数CPU上高效工作。

3

编译器可以执行一些分析和优化,并用下面的代码线程1结束:

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

对于单线程执行,该代码相当于原代码等是合法的。然后,如果Thread2的代码在步骤1和步骤2之间执行(这很可能),那么r3也被分配42。

此代码示例的整体思想是为了演示正确同步的需要。

+0

@Alexei这解释了一些。但是,编译器不应该使用'r3 = 0'而不是'r3 = 42'吗?或者他们只是在展示'一种可能性'! – gaganbm

+2

编译器不会生成'r3 = 42',它只会使'r3 = x'完好无损。编译器优化并不总是以最大深度执行。如果优化可能违反正确性的可能性很小,则会被放弃。在给定的代码中,不存在这样的情况,但是如果存在其他代码,则它们可以出现。另外,编译器可以决定'r3 = 0'与'r3 = x'的价格相同。 –