JVM 精髓happen before保证,go并不具备

关于 Happens-before,《Java 并发编程的艺术》书中是这样介绍的:

Happens-before 是 JMM 最核心的概念。对应 Java 程序员来说,理解 Happens-before 是理解 JMM 的关键。

《深入理解 Java 虚拟机 - 第 3 版》书中是这样介绍的:

Happens-before 是 JMM 的灵魂,它是判断数据是否存在竞争,线程是否安全的非常有用的手段。

我想,这两句话就已经足够表明 Happens-before 原则的重要性。

那为什么 Happens-before 被不约而同的称为 JMM 的核心和灵魂呢?

生来如此。

----

而Golang 的开发者就没有这么幸运了

网友说:
Golang 领域的一个明显问题是内存模型不提供 Happens-Before 保证。我编写了一个名为 Go-Disruptor (github.com/smartystreets/go-disruptor) 的项目,发现我不能保证线程之间的写入和读取顺序,这对于环形缓冲区的实现来说是绝对关键的。


    
阿列克西 :

> Golang 领域的一个明显问题是内存模型不提供 Happens-Before 保证。
它确实:https ://golang.org/ref/mem#tmp_2


    
jzelinskie :

你介意扩展一下吗?我从来没有实现过环形缓冲区,我真的很好奇。

    
永恒禁令 :

> 这与我通读环形缓冲区部分时的想法相同。
我很高兴看到这被称为。我对那篇文章也有同样精确的“等一下”的回应。


    
anon4 :

我还没有写过 Go,但据我所知,它可以让您混合使用特定于平台的程序集。你不能用它来设置内存屏障和/或发出你需要的特定加载/存储指令吗?

    
偏执狂 :

您可以,Go 运行时和标准库使用程序集进行许多操作。一个缺点是难以阅读和推理组装。C 风格的内在函数更容易。

----

Java 为什么能做到happen before保证呢?因为它是写在了 specification 文档中,相当于JVM的宪法。

让我们一起来看看怎么写的。

两个动作可以通过happens-before关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个动作之前排序。

如果我们有两个动作x 和y,我们写hb(x, y)来表示x 发生在 y 之前

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

  • 从对象的构造函数的末尾到该对象的终结器(第 12.6 节)的开头有一条发生前边缘。

  • 如果动作 x 与后续动作y同步,那么我们也有hb(x, y)

  • 如果hb(x, y)hb(y, z),则hb(x, z)

wait 类Object第 17.2.1 节)的方法具有与之关联的锁定和解锁操作;它们的happens-before关系由这些关联的动作定义。

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

例如,一个线程构造的对象的每个字段的默认值的写入不需要在该线程开始之前发生,只要没有读取观察到这个事实。

更具体地说,如果两个动作共享一个happens-before关系,那么对于它们不共享happens-before关系的任何代码,它们不一定必须以该顺序发生。例如,一个线程中的写入与另一个线程中的读取处于数据竞争中,这些读取可能会出现乱序。

发生之前的关系 定义了何时发生数据竞争。

一组同步边S足够的,如果它是最小集合,使得S的传递闭包与程序顺序决定了执行中的所有先发生边。这一套是独一无二的。

从上面的定义可以得出:

  • 监视器上的解锁发生在该监视器上的每个后续锁定 之前。

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

  • start()对线程的调用发生在已启动线程中的任何操作之前。

  • 线程中的所有操作都发生在任何其他线程从join()该线程上的 a 成功返回之前。

  • 任何对象的默认初始化发生在程序的 任何其他操作(默认写入除外)之前。

当一个程序包含两个冲突的访问(第 17.4.1 节),这些访问没有按照发生前的关系排序时,就可以说它包含数据竞争

线程间操作以外的操作语义,例如数组长度的读取(§10.7)、检查强制转换的执行(§5.5§15.16)和虚拟方法的调用(§15.12),不受数据的直接影响比赛。

因此,数据竞争不会导致错误的行为,例如返回错误的数组长度。

当且仅当所有顺序一致的执行都没有数据竞争时, 程序才能正确同步。

如果程序正确同步,则程序的所有执行都将显示为顺序一致(第 17.4.3 节)。

这对程序员来说是一个极其有力的保证。程序员不需要推理重新排序来确定他们的代码包含数据竞争。因此,在确定他们的代码是否正确同步时,他们不需要考虑重新排序。一旦确定代码正确同步,程序员就不必担心重新排序会影响他或她的代码。

程序必须正确同步以避免在重新排序代码时可以观察到的违反直觉的行为。使用正确的同步并不能确保程序的整体行为是正确的。但是,它的使用确实允许程序员以简单的方式推断程序的可能行为。正确同步的程序的行为对可能的重新排序的依赖性要小得多。如果没有正确的同步,可能会出现非常奇怪、令人困惑和违反直觉的行为。

我们说一个变量v的读取r被允许观察一个wv的写入,如果,在执行跟踪的发生之前的部分顺序中:

  • r不是在w之前排序的(即,不是hb(r, w)的情况),并且

  • 没有介入写入w ' 到v(即没有写入w ' 到v使得hb(w, w')hb(w', r))。

非正式地,如果没有发生之前的顺序来阻止读取 ,则允许读取r查看写入w的结果。

如果对于A 中的所有读取r (其中W(r)是 r看到的写入操作),一组操作A 是先发生后一致的,则不是hb(r, W(r)) 或存在在A中存在写入w使得wv = rv 和hb(W(r), w)hb(w, r)

发生前发生的一致操作中,每次读取都会看到一次写入,而发生前发生的顺序 允许它看到该写入。

示例 17.4.5-1。发生之前的一致性

对于表 17.4.5-A中的迹线,最初是A == B == 0。跟踪可以观察r2 == 0并且r1 == 0仍然是发生前一致的,因为有执行顺序允许每次读取看到适当的写入。

表 17.4.5-A。发生前一致性允许的行为,但顺序一致性不允许。

线程 1 线程 2
B = 1; A = 2;
r2 = A; r1 = B;

由于没有同步,每次读取都可以看到初始值的写入或其他线程的写入。显示此行为的执行顺序是:

1:B = 1;
3:A = 2;
2:r2 = A;// 看到初始写入 0
4:r1 = B;// 看到初始写入 0

另一个先发生后一致的执行顺序是:

1:r2 = A;// 看到 A = 2 的写入
3:r1 = B;// 看到 B = 1 的写入
2:B = 1;
4:A = 2;

在此执行中,读取会看到执行顺序中稍后发生的写入。这可能看起来违反直觉,但在发生之前的一致性允许。允许读取看到稍后的写入有时会产生不可接受的行为。

 

原文在:https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5

 

 

 

分类: 默认 标签: 发布于: 2022-06-21 19:05:07, 点击数: