Java内存模型:volatile变量和之前发生的

Ale*_*xey 36 java volatile java-memory-model thread-synchronization happens-before

我想澄清一下,在关系与volatile变量一起工作之前会发生什么.我们有以下变量:

public static int i, iDst, vDst;
public static volatile int v;
Run Code Online (Sandbox Code Playgroud)

和线程A:

i = 1;
v = 2;
Run Code Online (Sandbox Code Playgroud)

和线程B:

vDst = v;
iDst = i;
Run Code Online (Sandbox Code Playgroud)

以下语句是否符合Java内存模型(JMM)?如果没有,那么正确的解释是什么?

  • i = 1总是发生在以前 v = 2
  • v = 2 发生 vDst = v在JMM 之前,只有它实际发生在时间之前
  • i = 1 发生 iDst = i在JMM 之前(并且iDst可以预测分配1)如果v = 2实际发生vDst = v在时间之前
  • 否则,在i = 1和之间的顺序iDst = i是未定义的,结果值iDst也是未定义的

错误的逻辑:

有一个在JMM没有"挂钟时间"的概念,我们应该依靠同步顺序作为排序指南v = 2vDst = v.有关详细信息,请参阅所选答案.

use*_*ica 18

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

真正.按JLS第17.4.5节,

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


  • v = 2 发生 vDst = v在JMM 之前,只有它实际发生在时间之前
  • i = 1 发生 iDst = i在JMM 之前(并且iDst可以预测分配1)如果v = 2实际发生vDst = v在时间之前

假.事先发生的命令并不能保证物理时间内彼此之前发生的事情.从JLS的同一部分,

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

但是,保证v = 2 发生在之前 vDst = vi = 1 发生之前 - iDst = i如果在同步顺序之前发生,v = 2vDst = v对执行的同步动作的总顺序,这通常被误认为是实时顺序.


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

如果在同步顺序中vDst = v出现之前就是这种情况v = 2,但实际时间不会进入.


man*_*uti 10

是的,所有这些都是正确的根据本节关于发生 - 在订单之前:

  1. i = 1总是发生在以前 v = 2:

如果x和y是同一个线程的动作,并且x在程序顺序中出现在y之前,那么hb(x,y).

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

写入易失性字段(第8.3.1.4节) - 在每次后续读取该字段之前发生.

  1. i = 1 发生 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并不准确.在之前发生的关系并不一定具有强加这种关系的动作之间的定时顺序,如在JLS的相同部分中提到:

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

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

  • "后续"是根据同步顺序定义的,而不是根据物理时间来定义,因此我认为2和3都不完全正确.在真实的物理时间方面建立保证真的很难. (6认同)
  • @Blindy:"Happens-before"是一个特别定义的术语,与其他事物之前的物理事件几乎没有关系.此外,"随机文档"是语言规范. (3认同)

Zho*_*gYu 5

所有同步操作(易失性w/r,锁定/解锁等)形成总订单.[1]这是一个非常强烈的声明; 它使分析更容易.对于易失性v,要么在写入之前读取,要么在读取之前写入,在此总顺序中.订单取决于当然的实际执行情况.

从总订单中,我们可以建立之前发生的部分订单.[2]如果对变量(易失性或非易失性)的所有读写都在部分订单链上,则很容易分析 - 读取会看到前一次写入.这是JMM的要点 - 在读/写上建立订单,因此它们可以像顺序执行一样被推理.

但是如果易失性读取在易失性写入之前怎么办?我们需要另一个关键约束 - 读取不能看到写入.[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内传播30厘米; 在今天的CPU上,时间顺序绝对是相对的.总订单实际上要求从一个动作到下一个动作存在因果关系.这是一个非常强烈的要求,你敢打赌JVM会努力优化它.