Java 内存模型:易失性变量和发生前

Posted

技术标签:

【中文标题】Java 内存模型:易失性变量和发生前【英文标题】:Java memory model: volatile variables and happens-before 【发布时间】:2015-07-26 13:57:24 【问题描述】:

我想澄清 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),以下陈述是否正确? 如果不正确,正确的解释是什么?

i = 1 总是之前发生 v = 2 v = 2 happens-before vDst = v 在 JMM 中,前提是它实际上发生在时间之前 i = 1 之前发生 iDst = i 在 JMM 中(并且 iDst 将被分配 1)如果 v = 2 实际发生在 vDst = v 之前 i = 1iDst = i 之间的其他顺序未定义,iDst 的结果值也未定义

逻辑错误:

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

【问题讨论】:

没有什么可以添加到@manouti 答案中,但是如果您需要,这个问题会为您提供另一个示例:***.com/questions/17108541/… 可能重复:***.com/questions/11761552/… 【参考方案1】: i = 1 总是之前发生 v = 2

没错。由 JLS 部分17.4.5,

如果 xy 是同一线程的动作,并且 x 在程序顺序中位于 y 之前,然后 hb(x, y).


v = 2 happens-before vDst = v 在 JMM 中,前提是它实际上发生在时间之前 i = 1 发生在之前 iDst = i 在 JMM 中(并且 iDst 将被分配 1)如果 v = 2 实际发生在 vDst = v 之前

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

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

但是,如果v = 2 出现在@ 之前,则保证v = 2 happens-before vDst = vi = 1 happens-before iDst = i 987654337@在同步顺序中,一个执行的同步动作的总顺序,经常被误认为是实时顺序。


i = 1iDst = i 之间的其他顺序未定义,iDst 的结果值也未定义

如果vDst = v 在同步顺序中位于v = 2 之前,但实际时间并没有计算在内,就会出现这种情况。

【讨论】:

@user2357112,将“v = 2 发生在同步顺序中的 vDst = v 之前”替换为“vDst = v 是同步顺序中 v = 2 的后续操作”会更准确吗?该规范没有不同风格的“happens-before”(例如,同步顺序中的“generic”happens-before 和happens-before),因此可能会进一步混淆。否则,我发现您的答案给出了最准确的解释,我的答案不准确。 @Alexey:这两种措辞都是准确的。【参考方案2】:

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

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

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

    v = 2 happens-before vDst = v 在 JMM 中仅当它实际上发生在时间之前,因为 v 是不稳定的,并且

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

    i = 1 happens-before iDst = i 在 JMM 中(并且 iDst 将被分配为 1)如果 v = 2 实际发生在 vDst = 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 似乎并不准确。 happens-before 关系不一定在具有这种关系的动作之间强加一个时间顺序,如 JLS 的同一部分所述:

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

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

【讨论】:

"subsequent" 是按照同步顺序来定义的,而不是按照物理时间来定义的,所以我认为 2 和 3 并不完全正确。根据真实的物理时间建立保证真的很难。 @Blindy:“Happens-before”是一个专门定义的术语,与物理上发生在其他事情之前的事情几乎没有关系。此外,该“随机文档”是语言规范。 @Blindy gee.cs.oswego.edu/dl/jmm/cookbook.html 表明作为第二个操作的易失性存储不能与普通存储重新排序。当谈到 Java 和 volatile 时,你碰巧错了。 @user2357112,所以“对易失性字段(第 8.3.1.4 节)的写入发生在每次后续读取该字段之前。”方法?如何将其从规范翻译成简单的英语? @Alexey:如果可以找到从动作 A 到动作 B 的一系列发生前的关系,那么 B 必须看到 A 的效果,而 A 看不到 B 的效果。这并不能完全捕捉到整个概念,但这是我在限制条件下能做的最好的事情。【参考方案3】:

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

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

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

因此,我们可以推断,

    读取v 看到 0(初始化值)或 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 会努力优化它。

【讨论】:

以上是关于Java 内存模型:易失性变量和发生前的主要内容,如果未能解决你的问题,请参考以下文章

易失性关键字和线程本地内存[关闭]

易失性变量“读取”是不是与正常读取一样快?

对具有“易失性”属性的动态分配变量的内存访问是不是会导致每次访问的缓存未命中?

与 Java 中的 volatile 字段和同步块的关系发生在之前 - 以及它们对非易失性变量的影响?

PyTorch:RuntimeError:变量元组的元素0是易失性的

易失性和缓存行为