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

39

我想澄清一下volatile变量如何与happens-before关系配合工作。让我们假设有以下变量:

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之前发生(happens-before)。
  • 只有当v = 2实际上发生在时间上之前,v = 2才会在JMM中发生在vDst = v之前(happens-before)。
  • 如果v = 2实际上发生在时间上之前, 那么i = 1就会在JMM中发生在iDst = i之前(happens-before),且iDst将可预测地被赋值为1
  • 否则,i = 1iDst = i之间的顺序是未定义的,iDst的结果值也是未定义的。

逻辑错误:

JMM中没有“墙钟时间”的概念,我们应该依靠同步顺序作为v = 2vDst = v的排序指南。详见所选答案。


@manouti的回答已经很详细了,但如果您需要更多例子,这个问题可以为您提供:https://dev59.com/KGQn5IYBdhLWcg3wGD1s - uraimo
可能是重复问题:https://dev59.com/_Ggt5IYBdhLWcg3w3xEw - assylias
3个回答

20
  • i = 1总是发生在v = 2之前
  • 正确。根据JLS第17.4.5节,

    如果xy是同一线程的动作,并且x在程序顺序中出现在y之前,则hb(x,y)


    • 仅当它实际上按时间发生时,v = 2在JMM中才会发生在vDst = v之前
    • 如果v = 2实际上在时间上先于vDst = v发生,则在JMM中i = 1 happens-before iDst = i(并且将可预测地分配1iDst

    错误。 happens-before顺序不保证物理时间上的事情发生在彼此之前。来自JLS的相同部分,

    需要注意的是,两个动作之间存在happens-before关系并不一定意味着它们必须按那个顺序在实现中进行。如果重新排序产生与合法执行一致的结果,则不是非法的。

    然而,如果v = 2在同步顺序上先于vDst = v出现,即对执行的同步动作进行的全序,这通常被误认为是实时顺序,确保v = 2 happens-before vDst = vi = 1 happens-before iDst = i


    • 如果在同步顺序中vDst = v出现在v = 2之前,那么i = 1iDst = i之间的顺序是未定义的,iDst的结果也是未定义的。

    这种情况不考虑实际时间,只要vDst = v出现在v = 2之前即可。


    @user2357112,用“vDst = v是同步顺序中v = 2的后续操作”替换“v = 2在同步顺序中发生在vDst = v之前”,这样会更准确吗?规范没有不同类型的“happens-before”(例如“通用”的happens-before和同步顺序中的happens-before),因此可能会进一步混淆。否则,我认为您的答案给出了最精确的解释,我的答案中的不准确之处在于此。 - Alexey
    @Alexey:任何一种措辞都是准确的。 - user2357112

    10

    根据关于 happens-before 顺序的 此章节,它们全部都是正确的:

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

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

    1. v = 2 只有在实际时间上先于 vDst = v 的情况下,在 JMM 中才会 happens-before,因为 v 是 volatile 的,而且

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

    1. 只有当实际时间上 v = 2 先于 vDst = v 发生时,i = 1happens-before iDst = i 在 JMM 中(并且 iDst 将可预测地被赋值为 1)。因为在这种情况下:
      • i = 1 happens-before v = 2
      • v = 2 happens-before vDst = v
      • vDst = v happens-before iDst = i

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

    编辑:

    正如@user2357112所指出的,似乎语句2和3并不完全正确。正如JLS同一节中提到的那样,happens-before关系并不一定会在具有此关系的操作之间强制执行时间顺序:

    应当注意,在两个动作之间存在happens-before关系并不一定意味着它们必须按照实现中的顺序进行。如果重新排序产生与合法执行一致的结果,则不违法。

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


    6
    "subsequent"的定义是基于同步顺序而不是物理时间,因此我认为2和3并不完全正确。以真实物理时间作为保证是非常困难的。 - user2357112
    3
    “Happens-before”是一个专门定义的术语,与事物在物理上发生在其他事物之前没有太大关系。此外,“随机文档”是指语言规范。 - user2357112
    1
    @Blindy http://gee.cs.oswego.edu/dl/jmm/cookbook.html 显示,将volatile存储作为第二个操作无法与普通存储进行重新排序。但是,当涉及到Java和volatile时,你是错误的。 - John Vint
    @user2357112,那么"A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field."是什么意思?如何将其从规范语言翻译成通俗易懂的英语? - Alexey
    1
    @Alexey:如果可以找到从操作A到操作B的先于关系链,那么B必须看到A的影响,而A不能看到B的影响。这并没有完全捕捉到整个概念,但在限制条件下这是我能做到的最好的。 - user2357112
    @Alexey,规范要求volatile读取看到已经“推送”的任何volatile写入 - 但它不要求volatile写入立即被“推送”。而且这不能归因于硬件限制(光速等)。因此,如果写入需要N纳秒才能被“推送” - 那么就会有一个N纳秒的窗口,在这个窗口中,写入已经被“执行”,但读取可能会看到旧值。(这有点简单化,但有助于理解为什么你不能真正按照墙上时钟时间来推理)。另请参见:https://dev59.com/_Ggt5IYBdhLWcg3w3xEw - assylias

    6
    所有同步操作(volatile读写、锁定/解锁等)都形成了一个总序。[1] 这是一个非常强有力的声明,可以使分析更加容易。对于您的volatile v,在这个总序中,要么先读后写,要么先写后读。当然,顺序取决于实际执行。
    从总序中,我们可以建立“happens-before”部分序。[2] 如果变量(volatile或非volatile)上的所有读写都在部分序链上,那么就很容易进行分析——读取看到了直接前面的写入。这就是JMM的主要点——在读/写上建立顺序,以便像顺序执行一样推理。
    但是,如果volatile读在volatile写之前呢?我们需要另一个关键约束——读取不能看到写入。[3]
    因此,我们可以得出如下结论,
    1. 读取 v 看到的是0(初始值)或2(volatile写入)
    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中的一个动作对应于一场硬件活动风暴,不可能为其定义一个时间点)。

    甚至物理时间也不是绝对的。记住光在1纳秒内走30厘米;在今天的CPU上,时间顺序肯定是相对的。总顺序实际上要求从一个操作到下一个操作存在因果关系。这是一个非常强的要求,你可以打赌JVM会努力优化它。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接