Java 内存模型:volatile 变量和 happens-before

Java memory model: volatile variables and happens-before

我想阐明 happens-before 关系如何与 volatile 变量一起使用。让我们有以下变量:

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

和线程 A:

i = 1;
v = 2;

和线程 B:

vDst = v;
iDst = i;

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

逻辑错误:

JMM中没有"wall clock time"概念,我们应该依赖同步顺序作为v = 2vDst = v的排序指南.有关详细信息,请参阅所选答案。

是的,所有这些都是正确的 根据 this section 关于 happens-before 顺序:

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

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

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

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

  1. i = 1 先于 iDst = i 在 JMM 中(并且 iDst 将被预测分配 1)如果 v = 2 实际上及时发生在 vDst = v 之前。这是因为在这种情况下:
    • i = 1 先于 v = 2
    • v = 2 先于 vDst = v
    • vDst = v 先于 iDst = i

If hb(x, y) and hb(y, z), then hb(x, z).

编辑:

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

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

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

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

没错。按 JLS 部分 17.4.5

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).


  • v = 2 先于 vDst = v 在 JMM 中仅当它实际发生在时间之前
  • 如果 v = 2 实际上发生在时间 vDst = v 之前

错误。 happens-before 顺序并不能保证在物理时间内发生的事情先于彼此发生。来自 JLS 的同一部分,

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

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


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

如果在同步顺序中 vDst = vv = 2 之前,但实际时间不在其中,就会出现这种情况。

所有个同步动作(volatilew/r、lock/unlock等)组成一个总序。 [1] 这是一个非常有力的声明;它使分析更容易。对于你的 volatile v,要么读在写之前,要么写在读之前,在这个总顺序中。顺序当然要看实际执行了。

根据总顺序,我们可以建立部分顺序happens-before。 [2] 如果对变量(volatile 或非 volatile)的所有读取和写入都在偏序链上,则很容易分析 - 读取会看到紧接在前的写入。这是 JMM 的要点 - 在 read/writes 上建立订单,这样它们就可以像顺序执行一样推理。

但是如果易失性读取在易失性写入之前怎么办?我们在这里需要另一个关键约束——读不能看到写。 [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 会努力优化它。