【问题标题】:How does the happen-before property relate to visibility and ordering?之前发生的属性如何与可见性和顺序相关?
【发布时间】:2019-10-18 12:16:05
【问题描述】:

我想弄清楚发生前属性的确切含义。

我已经看到了对happens-before属性的解释说,如果全局变量(不是易失性或包含在同步块中)的更新在其他一些易失性变量之前被更改,则它们对其他线程可见,或者在同步块内改变。这是正确的吗?如果是这样,它在 java 文档中的什么地方是这样说的?

我的理解是,happens-before 属性定义了共享字段和代码执行之间的关系,例如:

  • 监视器的解锁发生在同一监视器的每次后续锁定之前。
  • 对 volatile 字段的写入发生在对同一字段的每次后续读取之前。
  • 在线程上启动的调用发生在已启动线程中的任何操作之前。
  • 线程中的所有操作都发生在任何其他线程从该线程的连接成功返回之前。

例如:

class Shared {

  private int y = 0;
  private volatile int x = 0;

  public void setOne() {
    y = 1;
    x = 1;
  }

  public int getY() {
    return y;
  }

}

对于上面的代码,给定 2 个线程:

Shared shared = new Shared();

new Thread(() -> shared.setOne()).start();
new Thread(() -> shared.getY()).start();

编辑 假设我们可以保证第一个线程已经启动,那么 getY() 会在这里返回 0 还是 1?

我还看到一些例子说这种行为只发生在读取线程中的 volatile 字段之后。因此,在这种情况下,如果一个线程读取 volatile 字段的值(假设是线程 B),那么线程 A 中在该 volatile 字段之前写入的所有字段都可用于线程 B。据此,如果我修改 getY() 方法上面的共享对象是:

  public int getXPlusY() {
    int local = x;
    return local + y;
  }

是这个动作让 y 对其他线程可见吗?

【问题讨论】:

  • 看这个,回答你所有的问题:youtube.com/watch?v=pS5dPQwgnYo
  • "getY() 会在这里返回 0 还是 1?"任何一个。不能保证在第二个线程启动时第一个线程已经启动。
  • 谢谢@Shadov,我会给它一块手表。
  • @AndyTurner,为了这个例子,假设我们可以保证第一个线程已经启动,它仍然可以吗?我将编辑问题以添加该假设。谢谢。
  • 正在启动的线程并不意味着它在任何其他线程可以(部分)执行之前执行到最后。即使在getY() 的开头有一个Thread.yield(),它最终也会返回零

标签: java java-memory-model


【解决方案1】:

让我们先看看你的第二个例子。

class Shared {

  private int y = 0;
  private volatile int x = 0;

  public void setOne() {
    y = 1; //(1)
    x = 1; //(2)
  }

  public int getXPlusY() {
    int local = x; //(3)
    return local + y; //(4)
  }
}

由于程序顺序,我们知道 (1) 和 (2) 之间存在 happens-before 关系:

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

由于x 是易变的,我们知道 (2) 和 (3) 之间存在 happens-before 关系

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

由于程序顺序,(3)和(4)之间又存在happens-before关系:

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

因此,我们有一个 happens-before 链 (1) → (2), (2) → (3), (3) → (4)

并且由于 happens-before 是传递关系(如果 A 发生在 B 之前,B 发生在 C 之前,那么 A 发生在 C 之前)这意味着 (1) 有一个 happens-before 与 (4) 的关系

现在让我们看第一个例子:

class Shared {

  private int y = 0;
  private volatile int x = 0;

  public void setOne() {
    y = 1; //(1)
    x = 1; //(2)
  }

  public int getY() {
    return y; //(3)
  }
}

在 (1) 和 (2) 之间还有 happens-before 关系,但仅此而已。因为x 没有被第二个线程读取,我们在(2) 和(3) 之间没有happens-before。因此,我们在 (1) 和 (3) 之间没有 happens-before 关系

引用来自Chapter 17 of the Java Language Specification (JLS)

【讨论】:

  • 1+,很好的答案。小的补充是,如果x 不是volatile,即:private int y = 0; private int x = 0;,则存在“程序顺序规则”,因此hb(x,y)。但这并不意味着它们将按该顺序执行。 perceivable 的执行顺序将是这样,但如果指令允许,它们可能完全以相反的顺序执行:在这种情况下,方法 setOne 确实允许这样做。
  • 还有一个问题是第二个例子可以正常工作,因为这部分of the JLS,特别是:对 volatile 变量 v 的写入与任何线程对 v 的所有后续读取同步(其中“后续”是根据同步顺序定义的)。用更简单的话来说:observed volatile 写入(某个线程看到的 volatile 读取)保证了在写入之前完成的所有事情,对读取线程都是可见的。
  • @Eugene 关于您的第二条评论 - 您从 JLS 中引用的部分需要阅读happens-beforesynchronizes-with all subsequent reads of v。在第二个示例中,getY() 不读取x,因此不能保证y 的值。所以虽然这段代码可以工作,但不能保证。
  • @Eugene 关于第一条评论 - 你是对的,但实际的执行顺序并不重要。这里的讨论是关于这些行动的影响的可见性。关于有问题的具体代码,我不认为编译器可以精确地重新排序12 由于易失性读取的影响。
  • 我不知道您是否故意这样做,但是您对问题和 cmets 的回答是相反的,包括初始答案和我的 cmets。我的第一条评论针对的是 OP 的第一个示例:class Shared { private int y = 0; private volatile int x = 0; public void setOne() { y = 1; //(1) x = 1; //(2) } public int getY() { return y; //(3) } },我说 if x 不会不稳定...,因此您用 回复的部分...由于 volatile read 的影响在这种情况下没有任何意义,真的。我的整个论点是
【解决方案2】:

我将重命名这些类以使其清楚:

class First {

    private int y = 0;
    private volatile int x = 0;

    public void setOne() {
       y = 1;
       x = 1;
    }

    public int getY() {
       return y;
    }

}

你的前提是:

假设我们可以保证第一个线程已经启动

零保证getY 将返回1。只有当您遵守 JLS 的先发生规则时,才能保证对共享字段的更新对某些读取线程可见,此处不强制执行。因此,从 getY 获取 0 是完全有效的。

你的第二个例子:

class Second {

    private int y = 0;
    private volatile int x = 0;

    public void setOne() {
       y = 1;
       x = 1;
    }

    public int getXPlusY() {
       int local = x;
       return local + y;
    }

}

同样的前提是:

假设我们可以保证第一个线程已经启动

保证返回2。对于输出2 的方法,读取线程必须观察(查看)易失性写入。那就是它必须是这样的:

 public int getXPlusY() {
   int local = x;
   if(local == 1){ // you need to see the write that was done in the other thread
      return local + y;
   }
   return local + y;    
}

如果您输入 if 语句 f(local == 1),您已经观察到在另一个线程 (x = 1) 中完成的易失性写入,因此,您可以保证在该写入之前完成的所有内容也是可见的。这意味着在这个时间点,y = 1,当然,所以你的方法将返回2

没有这个 if 语句,你的阅读 int local = x 是一个活泼的阅读,没有什么可以阻止这个线程在两者之间做 int local = x

y = 1;
x = 1;

事实证明这也是可以证明的,这是一个 jcstress 测试,证明了另一个答案(至少 1/2)是错误的:

// https://stackoverflow.com/questions/58450777/how-does-the-happen-before-property-relate-to-visibility-and-ordering
@JCStressTest
@State
@Outcome(id = "0", expect = Expect.FORBIDDEN, desc = "can not happen")
@Outcome(id = "1", expect = Expect.FORBIDDEN, desc = "can not happen")
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "reader thread was done before writer did anything")
@Outcome(id = "3", expect = Expect.ACCEPTABLE_INTERESTING, desc = "racy read!!!")
@Outcome(id = "4", expect = Expect.ACCEPTABLE, desc = "reader thread sees everything that writer did")
public class VolatileRace {

    // change defaults so that "readerThread" does not output a false positive.
    // it can happen when it runs it's entire method body before "writerThread"
    // does anything at all.
    private int y = 1;
    private volatile int x = 1;

    @Actor
    public void writerThread() {
        y = 2;
        x = 2;
    }

    @Actor
    public void readerThread(I_Result result) {
        int local = x;
        result.r1 = local + y;
    }

}

这个测试的重点是,它有这样一个输出:

  0             0                FORBIDDEN  can not happen
  1             0                FORBIDDEN  can not happen
  2     4,616,926               ACCEPTABLE  reader thread was done before writer did anything
  3         6,694   ACCEPTABLE_INTERESTING  racy read!!!
  4     3,107,831               ACCEPTABLE  reader thread sees everything that writer did

即使你不明白,它也表明阅读线程已经看到3作为它的结果,这意味着它已经阅读了x = 1y = 2

【讨论】:

    猜你喜欢
    • 2011-09-22
    • 2011-03-29
    • 2015-07-19
    • 1970-01-01
    • 2017-08-31
    • 2013-02-04
    • 1970-01-01
    • 2015-11-09
    • 2012-05-15
    相关资源
    最近更新 更多