Java内存模型:volatile变量并发生在

时间:2015-05-14 19:45:16

标签: java volatile thread-synchronization java-memory-model happens-before

我想澄清 volatile 变量与 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 在JMM中发生 iDst = i之前(如果iDst实际发生1v = 2将被预测分配vDst = v时间i = 1之前
  • 否则iDst = iiDst之间的顺序未定义,v = 2的结果值也未定义

逻辑上的错误:

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

3 个答案:

答案 0 :(得分:18)

  • i = 1始终发生在 v = 2
  • 之前

真。按JLS部分17.4.5

  

如果 x y 是同一个线程的操作, x 按照程序顺序出现在 y 之前,然后 hb(x,y)

  • v = 2 发生在JMM之前 vDst = v,只有在实际发生时才会发生
  • i = 1 在JMM中发生 iDst = i之前(如果iDst实际发生1v = 2将被预测分配vDst = v时间v = 2之前

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

  

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

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

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

如果vDst = v在同步顺序中位于v = 2之前,则会出现这种情况,但实际时间不会进入。{/ p>

答案 1 :(得分:10)

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

  1. i = 1始终发生在 v = 2之前:
  2.   

    如果x和y是同一个线程的动作,x在程序顺序中位于y之前,那么 hb(x,y)

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

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

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

        如果 hb(x,y) hb(y,z),那么 hb(x,z)

        修改

        正如@ user2357112所说,似乎陈述2和3并不准确。 发生 - 之前关系并不一定会在具有此关系的操作之间强制执行时间顺序,如JLS的相同部分所述:

          

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

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

答案 2 :(得分:5)

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

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

但是如果易失性读取在易失性写入之前怎么办?我们需要另一个关键约束 - 读取不能看到写入。 [3]

因此,我们可以推断,

  1. 读取v看到0(初始值)或2(易失写入)
  2. 如果它看到2,则必须是在写入之后读取的情况;在这种情况下,我们有happens-before链。
  3. 最后一点 - 读取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会努力优化它。