【问题标题】:Why is 'happens-before; relationship called like that?为什么'发生在之前;关系叫这样吗?
【发布时间】:2020-08-18 16:03:31
【问题描述】:

我理解这个概念的所有内容,除了为什么这样称呼它。有人可以帮我理解吗?它仍然让我感到困惑..这是我唯一的问题。 看了几篇文章,还是想不通它的成名动机。

【问题讨论】:

  • 你明白“happens before”的简单英文意思吗?
  • happens-before 关系仅仅意味着A 发生在B 之前。因此,例如语句A 将在语句B 之前由CPU 执行。所以它只是简单的英语单词的意思,就是这样。
  • 我认为将其视为“似乎以前发生过”是有用的。也就是说,如果“A 发生在 B 之前”,那么 B 发生时 A 似乎已经发生了。
  • 既然你说你除了名字之外什么都懂,而且不明白他们为什么这样称呼它——你提议的名字是什么?这可能有助于理解您的困惑。
  • “A 代码可见性 B”听起来不像规范中使用的短语的替代品。它是一个关系运算符,“A happens-before B”可以这样理解。他们本可以发明一个实际的运算符,例如“A ≺ B”但是,人们仍然会问如何发音。

标签: java multithreading happens-before


【解决方案1】:

阅读this article 可能会有所帮助。

一般要点是:JMM 的定义是“这组有限的事件被定义为暗示一件事总是在另一件事之前发生”,这就是“发生在之前”一词的来源。然而,归结为,逻辑上的这种跳跃:“如果 JMM 说 A 发生在 B 之前,那实际上意味着 B 之后的所有代码必须能够观察到 一切一种。”。计时规则变成观察规则。

但是这个观察规则是你可能学到的,也是你理解 JMM 的方式,这很好。但是,我假设'如果你添加一个同步块,这意味着其他线程会一致地观察你的变化,而如果你不这样做,就不能保证它们会'似乎与英文单词'comes before'无关.但现在你知道了。

再深入一点

VM 希望能够对线程内和线程间的操作重新排序,因为这为优化打开了大门。但是,有时重新排序会破坏应用程序。那么 VM 怎么知道不重新排序(VM 怎么知道重新排序会破坏它)?如果 VM 意识到这 2 个事件之间存在时间关系,则不要对 2 个事件重新排序 - 当这两个事件依赖于一个事件应该发生在另一个事件之前。然后,JMM 打破了哪些语言结构创建了这种时序关系,并要求我们的 Java 编码人员编写我们的应用程序,以便如果我们依赖特定的顺序,我们使用其中一个定义的发生在关系之前,以便 VM 知道并获胜。 t 重新订购我们。

这是三件事:

  • 命令式:在单个线程中,所有语句都发生在所有其他语句之前 - 这是显而易见的。在:{x(); y();} 中,VM 假定 java 代码依赖于在 y() 调用之前发生的 x() 调用,无论 x 和 y 是什么。

  • java.lang.Thread:在线程实际启动之前,在线程对象上调用 .start()。如果一个线程 .join()s 另一个线程,则另一个线程中的所有操作都在 join() 返回之前发生。

  • 同步原语 - 同步:如果您在对象 FOO 上的 synchronized() 块结束,代码依赖于这样一个事实,即在任何其他线程通过以下方式获取锁之前,这已完全完成开始同步(FOO)。

  • 同步原语 - volatile:字段写入发生在以后的 volatile 字段读取之前。

那么让我们回到它的真正含义,通过最后一个:它似乎是同义词,不是吗?这就是说:“一件事发生在另一件事之前,意味着另一件事发生在之后”。这就像“圆是圆的”。但它涉及到这些东西的意图和它的真正含义:

这与实际执行时间无关。这是关于能够见证它的影响

易失性读/写是说:

如果线程 A 碰巧写入了 volatile,而 B 碰巧看到了该写入,那么这意味着 A 所做的任何其他事情,无论是否是 volatile/同步,也必须对 B 可见。

因此,我们已经从“时间关系”转向“可见性关系”,而后者正是 JMM 的意义所在,并且大概是您理解它的方式。希望现在您了解我们如何从“时间”获得“可见性”(并且“发生在之前”很明显,因为“它是关于时间的”,大概)。

这是一个例子:

class Example {
    public int field1 = 0;
    public int field2 = 0;

    public void runInA() {
        int f1 = field1, f2 = field2;
        field1 = 5;
        field2 = 10;
        System.out.println(f1 + " " + f2);
    }

    public void runInB() {
        int f1 = field1, f2 = field2;
        field1 = 20;
        field2 = 40;
        System.out.println(f1 + " " + f2);
    }
}

在这里,VM 最终打印是可以接受的:

0 0
0 40

但这似乎没有任何意义! (这里线程 B 在 A 之前运行) - 但不知何故,第二个字段 write 是可见的,但第一个不是?嗯? - 但这就是它的工作原理,JMM 不做任何保证。

不过,如果再添​​加一个 volatile 写入,您将无法再观察到这一点。

【讨论】:

  • 关于你的“3 件事”(实际上是 4 个),因为 java-9 也有直接的release/acquire。但是,“扔在一个 volatile write”是错误的。 happens-before 作用于两个 读取和写入,为动作配对。
【解决方案2】:

A发生在B之前并不意味着A发生在B之前。

你是对的。我同意,这令人困惑。

当您在 Java 语言规范 (JLS) 中看到“A 发生在 B 之前”时,您应该将其理解为“程序必须表现得好像 A 发生在 B 之前”。 p>

他们称之为“发生在之前”的原因是为了强调这是一个transitive relationship:如果你知道A“发生在”B之前,并且你也知道B“发生在之前” C,那么你可以推断出A“发生在”C之前。也就是说,程序必须表现得好像A实际上发生在C之前。

这是一个例子:一条规则说,

An unlock on a monitor happens-before every subsequent lock on that monitor.

看起来很明显!这个怎么样?

If x and y are actions of the same thread and x comes before y in program
order, then [x happens before y].

我们还需要这么说吗?是的!因为如果你把它们放在一起,如果你记得默默地插入“程序必须表现得好像”,那么你可以结合这两个规则来得出一个有用的结论:

假设线程 1 将值存储到变量 a、b 和 c 中;然后它随后解锁了一个锁。还假设一段时间后,线程 2 锁定了同一个锁,然后它检查 a、b 和 c。

第一条规则说程序必须表现得好像你为线程 1 编写的代码实际上按照你告诉它的顺序做了你告诉它做的所有事情它们(例如,必须在解锁锁之前分配 a、b 和 c,)。

第二条规则说 *IF* 线程 2 实际上确实锁定了锁线程 1 释放它之后,那么程序 必须表现得好像 em> 事情真的是按这个顺序发生的。而且,回到第一条规则,程序 必须表现得好像线程 2 在检查 a、b 和 c 之前 获得了锁。

把它们放在一起,你可以得出结论,程序必须表现得好像线程1写了a、b和c,线程2查看它们之前。

另外,A发生在B之前并不意味着A发生在B之前

正确。例如,让我们把锁拿走。

如果线程 1 写入了一些变量,然后在线程 2 检查相同的变量之前经过了一些实际时间,但没有锁定,也没有其他任何东西可以建立“发生在之前”关系链;那么程序不需要表现得好像写入发生在读取之前。注意!在现代多处理器系统上,实际上很可能第二个线程可能会看到 a、b 和 c 的不一致视图(例如,好像第一个线程更新了 a,但没有更新 b,或者c.)

“发生在之前”规则是一个正式的系统,它定义了 Java 程序的行为方式。如果您找不到一连串“之前发生过”来证明您的程序将以您希望的方式运行,那么您的程序有误:JLS 不要求它的行为方式与您认为的一样。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多