39

我想澄清发生前的关系如何与volatile变量一起工作。让我们有以下变量:

public static int i, iDst, vDst;
public static volatile int v;

和线程A:

i = 1;
v = 2;

和线程 B:

vDst = v;
iDst = i;

根据 Java 内存模型 (JMM),以下陈述是否正确?如果不是,正确的解释是什么?

  • i = 1总是发生在之前 v = 2
  • v = 2 在 JMM 中发生之前 vDst = v,仅当它实际上发生在时间之前
  • i = 1 如果实际发生在时间之前iDst = i,则在 JMM 中发生(并且iDst将被可预测地分配)1v = 2vDst = v
  • 否则 and 之间的顺序i = 1iDst = i未定义的,结果值 ofiDst也是未定义的

逻辑错误:

JMM 中没有“挂钟时间”的概念,我们应该依赖同步顺序作为 和 的排序v = 2指南vDst = v。有关详细信息,请参阅所选答案。

4

3 回答 3

20
  • i = 1总是发生在之前 v = 2

真的。通过 JLS 第17.4.5 节

如果xy是同一线程的操作,并且x在程序顺序中位于y之前,则为 hb(x, y)


  • v = 2 在 JMM 中发生之前 vDst = v,仅当它实际上发生在时间之前
  • i = 1 如果实际发生在时间之前iDst = i,则在 JMM 中发生(并且iDst将被可预测地分配)1v = 2vDst = v

错误的。发生之前的顺序并不能保证在物理时间之前发生的事情。从 JLS 的同一部分,

应该注意的是,两个动作之间存在之前发生的关系并不一定意味着它们必须在实现中以该顺序发生。如果重新排序产生与合法执行一致的结果,则不是非法的。

但是,保证v = 2 发生之前 vDst = vi = 1 发生之前 iDst = i如果在同步顺序中v = 2出现在之前vDst = v,则执行的同步操作的总顺序经常被误认为实时顺序。


  • 否则 and 之间的顺序i = 1iDst = i未定义的,结果值 ofiDst也是未定义的

如果在同步顺序vDst = v之前出现这种情况v = 2,但实际时间没有进入它。

于 2015-05-14T20:43:49.867 回答
10

是的,根据本节关于发生前的顺序,所有这些都是正确的:

  1. i = 1总是发生之前, v = 2因为:

如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)

  1. v = 2 在 JMM 中发生之前 vDst = v,仅当它实际上发生在时间之前,因为v它是易变的,并且

对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。

  1. i = 1 如果实际发生在时间之前iDst = i则在 JMM 中发生(并且iDst可以预见地分配为 1)。这是因为在这种情况下: v = 2vDst = v
    • i = 1 发生之前 v = 2
    • v = 2 发生之前 vDst = v
    • vDst = v 发生之前 iDst = i

如果hb(x, y)hb(y, z),则hb(x, z)

编辑:

正如@user2357112 所争论的那样,似乎陈述 2 和 3 并不准确。如JLS 的同一部分所述, happens-before关系不一定在具有这种关系的动作之间强加时序顺序:

应该注意的是,两个动作之间存在之前发生的关系并不一定意味着它们必须在实现中以该顺序发生。如果重新排序产生与合法执行一致的结果,则不是非法的。

因此,就 JLS 中提到的规则而言,我们不应该对语句执行的实际时间做出假设。

于 2015-05-14T20:00:01.617 回答
6

所有同步操作(volatile w/r、锁定/解锁等)形成一个总顺序。[1] 这是一个非常有力的声明;它使分析更容易。对于您的 volatile v,按此总顺序,读在写之前,或者写在读之前。顺序当然取决于实际执行。

从该总顺序中,我们可以建立部分顺序发生在之前。[2] 如果对一个变量(无论是否为易失性)的所有读取和写入都在偏序链上,则很容易分析——读取会看到紧接在前的写入。这就是 JMM 的要点——建立读/写顺序,这样它们就可以像顺序执行一样被推理。

但是如果 volatile 读在 volatile 写之前呢?这里我们需要另一个关键的约束——读不能看到写。[3]

因此,我们可以推断,

  1. 读取v看到 0(初始值)或 2(易失性写入)
  2. 如果看到 2,则一定是读在写之后;在这种情况下,我们有happens-before链。

最后一点 - 读取i必须看到其中一个写入i;在这个例子中,0 或 1。它永远不会看到不是来自任何写入的魔法值。


引用 java8 规范:

[1] http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.4

[2] http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5

[3] http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.7


关于总订单的随机想法:

由于这个总顺序,我们可以说一个同步操作发生在另一个之前,就好像及时一样。那个时间可能与挂钟不对应,但它对我们的理解来说并不是一个糟糕的心理模型。(实际上,java中的一个动作对应着一场硬件活动风暴,不可能为它定义一个时间点)

甚至物理时间也不是绝对的。请记住,光在 1ns 内传播 30cm;在今天的 CPU 上,时间顺序绝对是相对的。总顺序实际上要求从一个动作到下一个动作之间存在因果关系。这是一个非常强烈的要求,你敢打赌 JVM 会努力优化它。

于 2015-05-14T21:10:41.297 回答