发生在 Java 内存模型之前和程序顺序

Happens before and program order in Java Memory Model

我对程序顺序及其如何影响 JMM 中的重新排序有一些疑问。

在 Java 内存模型中,程序顺序 (po) 被定义为程序中每个线程的总操作顺序。根据 JLS,这会导致 happens-before (hb) 边:

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y) (i.e. x happens-before y).

所以对于一个简单的程序P:

  initially, x = y = 0
     T1     |     T2
 -----------|-----------
  1. r1 = x | 3. r2 = y  
  2. y = 1  | 4. x = r2

我认为 po(1, 2)po(3, 4)。因此,hb(1, 2)hb(3, 4).

现在假设我想对其中一些语句重新排序,得到 P':

  initially, x = y = 0
     T1     |     T2
 -----------|-----------
  2. y = 1  | 3. r2 = y  
  1. r1 = x | 4. x = r2

根据 this paper,我们可以重新排序任何两个相邻的语句(例如 1 和 2),前提是重新排序不会消除任何有效执行中的任何传递发生前边。但是,由于 hbpo(部分)定义,并且 po 是一个总阶thread 的行为,在我看来,在不违反 hb 的情况下不可能重新排序任何两个语句,因此 P' 不是合法的转换。

我的问题是:

  1. 我对pohb的理解是否正确,我是否正确定义了pohb 关于上面的程序 P?
  2. 我对 hb 重新排序失败的理解在哪里?

您缺少 JLS 的这一部分:

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

在您的情况下,由于 1 和 2 不相关,因此可以翻转它们。现在如果 2 已经 y = r1,那么 1 必须在 2 之前发生才能得到正确的结果。


真正的问题出现在多处理器执行上。在没有任何先行边界的情况下,无论执行顺序如何,T2 都可能观察到 2 发生在 1 之前。

这是因为 CPU 缓存。假设 T1 以任意顺序执行了 1 和 2。由于不存在 happen-before 边界,这些操作仍在 CPU 缓存中,并且根据其他需要,包含 2 结果的缓存部分可能会在包含结果的缓存部分之前刷新1.

如果 T2 在这两个缓存刷新事件之间执行,它将观察到 2 发生了而 1 没有发生,即据 T2 所知,2 发生在 1 之前。

如果不允许,则 T1 必须在 1 和 2 之间建立一个 happens-before 边界。

在 Java 中有多种方法可以做到这一点。旧的风格是将 1 和 2 放入单独的 synchronized 块中,因为 synchronized 块的开始和结束是一个先行边界,即块之前的任何动作都先于块内的动作发生块,并且块内的任何操作都发生在块之后的操作之前。

你说的P',其实不是一个不同的程序,而是同一个程序P的执行轨迹,可能是不同的程序,但是会有不同的po ,因此不同 hb.

Happens-before 关系限制语句根据其可观察到的效果重新排序,而不是根据它们的执行顺序。动作 1 happens-before 2,但他们不观察对方的结果,所以他们被允许重新排序。 hb 保证您会观察到两个动作是按顺序执行的,但仅来自同步上下文(即来自形成 hb 和 [= 的其他动作) 19=]1 和 2)。你可能会想到 12 说:Let's swap。没人看!.

这是 JLS 的一个很好的例子,很好地反映了 happens-before 的想法:

For example, the write of a default value to every field of an object constructed by a thread need not happen before the beginning of that thread, as long as no read ever observes that fact

在实践中,几乎不可能在线程启动之前对线程构造的所有对象进行默认值写入,即使它们在每个操作中形成 同步 边缘在那个线程中。一个启动线程可能不知道它在 运行 时间内将构建什么以及多少个对象。但是一旦你有一个对象的引用,你会发现默认值写入已经发生。对尚未构建(或已知已构建)的对象的默认写入顺序通常无法反映在执行中,但它仍然不违反 happens-before 关系,因为它是关于可观察的效果。

我认为一个关键问题是你的结构P'。这意味着重新排序的工作方式是重新排序是全局的——整个程序以单一方式重新排序(在每次执行时),它服从内存模型。然后你试图对此进行推理 P' 并发现不可能进行有趣的重新排序!

实际发生的情况是,对于与 hb 关系无关的语句,没有特定的全局顺序,因此不同的线程在同一执行中可以看到不同的表观顺序。在您的示例中,一组中的 {1,2} 和 {3,4} 语句之间没有边可以按任何顺序看到另一组中的语句。例如,T2 可能在 1 之前观察到 2,但随后观察到 T3,这与 T2 相同(具有自己的私有变量) ,观察相反!所以没有单一的重新排序 P' - 每个线程都可以观察到自己的重新排序,只要它们符合 JMM 规则。