如何理解 happens-before 一致性

15

JLS第17章中,它介绍了一个概念:happens-before一致。

如果对于A中所有的读操作r,其中W(r)是r所看到的写操作,则集合A是happens-before一致的,那么既不是hb(r,W(r))也不是存在一个写操作w属于A,使得w.v=r.v并且hb(W(r),w)和hb(w,r)“

根据我的理解,它相当于以下表述: … ,它不是hb(r,W(r))也不是存在一个写操作w属于A,使得w.v=r.v并且hb(W(r),w)和hb(w,r)

因此,我有两个问题:

  • 我的理解正确吗?
  • “w.v = r.v”是什么意思?

它还给出了一个示例:17.4.5-1。

Thread 1 Thread 2

B = 1; A = 2; 

r2 = A; r1 = B; 

按照第一执行顺序:

1: B = 1;

3: A = 2;

2: r2 = A;  // sees initial write of 0

4: r1 = B;  // sees initial write of 0

根据顺序本身,我们已经知道两个线程是交替执行的,因此我的第三个问题是:左边的数字代表什么意思?

在我看来,r2和r1都能看到0的初始写入,原因是A和B都不是易失字段。因此,我的第四个问题是:我的理解是否正确?

在第二次执行顺序中:

1: r2 = A;  // sees write of A = 2

3: r1 = B;  // sees write of B = 1

2: B = 1;

4: A = 2;

根据“先行发生一致性”的定义,很容易理解这个执行顺序是符合“先行发生一致性”的(如果我的第一个理解是正确的)。 那么我的第五和第六个问题是:在现实世界中是否存在此类情况(读取看到稍后发生的写入)?如果存在,请给我一个真实的例子。


我需要对这个例子进行澄清...如果两个线程的赋值都在读取之前,那么它们到达读取之前至少有一个值被更改了。那么这两个线程怎么会看到0呢? - treaz
严肃点,左边的数字是什么意思? - Tim
5个回答

19

每个线程可以位于不同的核心上,具有自己的私有寄存器,Java可以使用这些寄存器来保存变量值,除非您强制访问一致的共享内存。这意味着一个线程可以写入一个存储在寄存器中的值,而另一个线程在某段时间内(如循环或整个函数的持续时间)看不到这个值。(毫秒的时间并不罕见)

更极端的例子是,读取线程的代码被优化为假设它从未更改该值,因此它不需要从内存中读取它。在这种情况下,优化的代码永远不会看到另一个线程执行的更改。

在这两种情况下,使用volatile可以确保读取和写入按一致的顺序发生,两个线程看到相同的值。有时将其描述为总是从主内存读取,尽管这不一定是最终情况,因为缓存可以直接彼此通信。(因此性能影响比您想象的要小得多)。

在正常的CPU上,缓存是"一致的"(不能保留过期/冲突的值)和透明的,不需要手动管理。使数据在线程之间可见只意味着在汇编语言中执行实际的加载或存储指令来访问内存(通过数据缓存),并可选择等待存储缓冲区排空以便于其他后续操作排序。


非常感谢您的回答。我可以认为我的两个理解都是正确的吗?还有两个问题:"w.v = r.v" 是什么意思?左边的数字是什么意思?再次感谢。 - newman
根据我的理解,我发现如果程序正确同步,它仍然允许程序中存在数据竞争。那么你对这个观点有什么看法? - newman
我已经开始了一个新的问题:即使程序正确同步,是否仍然允许数据竞争?(第一部分)欢迎您加入新问题的讨论。 - newman
1
synchronized 确保块入口和出口处的所有内存以可预测的顺序发生,即使您没有在块中访问过这些内容。同步组件允许不同的竞争条件,例如多次访问的 Vector 或 Collections.syncrhonizedList 可能存在读写该集合时的竞争条件。 - Peter Lawrey

8

happens-before

在并发理论中,happens-before(先于关系)是一种内存屏障(memory barrier),有助于解决可见性(Visibility)和排序(Ordering)问题。它是一种线程间的偏序关系,如果一个操作在另一个操作之前执行,那么这个操作就是在另一个操作之前发生(happens-before)的。例如,volatile[参考]synchronized monitor[参考]都是良好的例子。

让我们来看看并发理论中的定义:

Atomicity(原子性) - 是一种操作特性,可以完全作为单个事务执行,不能部分执行。例如 原子操作[示例]

Visibility(可见性) - 如果一个线程进行了更改,则其他线程可以看到这些更改。在Java 5之前使用volatile与。

Ordering(顺序性) - 编译器能够更改源代码的操作/指令顺序以进行一些优化。

原子性的一个很好的例子是比较并交换(CAS)模式的实现,它应该是原子的,并允许在多线程环境中更改变量。如果想要自己实现CTA,可以使用以下方式:

  • volatile + synchronized
  • java.util.concurrent.atomicJava 5中的sun.misc.Unsafe(内存分配,无构造函数调用的实例化...),使用了JNI和CPU优势。

CAS算法具有三个参数(A(地址),O(旧值),N(新值))。

If value by A(address) == O(old value) then put N(new value) into A(address), 
else O(old value) = value from A(address) and repeat this actions again

先行发生

官方文档

两个动作可以通过先行发生关系来进行排序。如果一个动作先行发生于另一个动作,则第一个动作对第二个动作是可见的并且被排序在第二个动作之前。

enter image description here

使用volatile[关于]作为示例

volatile 字段的 写入 发生在每个后续的 读取 之前。

让我们看一个例子:

// Definitions
int a = 1;
int b = 2;
volatile boolean myVolatile = false;

// Thread A. Program order
{
    a = 5;
    b = 6;
    myVolatile = true; // <-- write
}

//Thread B. Program order
{
    //Thread.sleep(1000); //just to show that writing into `myVolatile`(Thread A) was executed before

    System.out.println(myVolatile); // <-- read
    System.out.println(a);  //prints 5, not 1
    System.out.println(b);  //prints 6, not 2
}

可见性 - 当线程A 改变/写入一个volatile变量时,它将所有之前的更改推送到RAM - 主内存中,因此所有非volatile变量都将是最新的并且对其他线程可见。

排序

  • 在写入线程A的volatile变量之前的所有操作都将被称为之前的操作。JVM能够重新排序它们,但保证在线程A写入volatile变量之前不会有任何一个操作被调用后它。

  • 在读取线程B的volatile变量之后的所有操作都将被称为之后的操作。JVM能够重新排序它们,但保证在线程B读取volatile变量之后不会有任何一个操作被调用在它之前。

[并发与并行]


2
在排序方面,不仅编译器会改变排序或感知排序(指令可能按顺序执行,但它们对内存的影响可能不会被其他核心/ CPU以相同的顺序看到)。CPU的流水线架构也可以并且确实会改变彼此输出无关的指令的排序。 - Erwin Bolwidt
@yoAlex5 我理解你的评论。但是为什么下面的代码不会陷入无限循环呢? public class HB { public static Boolean flag = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (flag) { System.out.println( "do=====>"); } System.out.println(Thread.currentThread().getName() + " " + "end"); }, "server"); t.start(); Thread.sleep(1000); System.out.println("parent"); flag = false; }} - study_20160808

4
Java内存模型定义了程序中所有操作的一个部分顺序,称为“发生在(happens-before)”。
为了保证线程Y能够看到操作X的副作用(无论X是否发生在不同的线程中),在X和Y之间定义了“发生在”关系。
如果不存在这样的关系,JVM可能会重新排列程序的操作。
现在,如果一个变量由许多线程共享并被访问,并且至少有一个线程写入,如果读写操作没有按照“发生在”关系排序,则会产生数据竞争。
在正确的程序中,不存在数据竞争。
例如,两个线程A和B在锁X上同步。
线程A获取锁(此时线程B被阻塞),执行写操作,然后释放锁X。现在线程B获取锁X,并且由于线程A的所有操作都是在释放锁X之前完成的,它们在线程B之前被排序,线程B在获取锁X之后执行(并且对线程B可见)。
请注意,这仅适用于在“相同锁”上同步的操作。线程在“不同锁”上同步时不存在“发生在”关系。

非常感谢您的回答。我可以认为我的两个理解都是正确的吗?还有两个问题:"w.v = r.v" 是什么意思?左边的数字是什么意思?再次感谢。 - newman
1
@newman:r.v 表示:读取变量 v,而 w.v 表示:写入变量 v。在同一段落中解释了它们的含义:如果在执行轨迹的 happens-before 部分顺序中,允许一个变量 v 的读取 r 观察到一个写入 w,则我们称变量 v 的读取 r 被允许观察变量 v 的写入 w。 - Cratylus
@newman 你可能会对这个讨论感兴趣,它展示了如何使用该段落来证明并发代码的正确性(即无数据竞争)。 - assylias
非常感谢您的回复。我已经理解了w.v和r.v的含义,但"w.v = r.v"意味着什么呢?换句话说,等号“=”代表什么?我认为它不是指“相等”,因为“相等”应该使用“==”符号。所以,请您再进一步解释一下。 - newman
@newman 这意味着读取和写入操作针对的是同一变量 - 它并没有说明被写入 / 读取的变量的值。 - assylias
显示剩余3条评论

2
实际上是这样的。需要注意的主要内容是:除非使用某种形式的同步,否则不能保证程序中一个写操作在后面出现的读操作可以看到该写操作的效果,因为语句可能已经被重新排序。
“在现实世界中是否存在这种情况(读取看到稍后发生的写入)?如果存在,您能给我一个真实的例子吗?”
从墙钟的角度来看,很明显,读取不能看到尚未发生的写入的效果。
从程序顺序的角度来看,因为语句可以重新排序,如果没有适当的同步(先于关系),则在程序中一个写操作之前出现的读操作可能会在执行期间看到该写操作的效果,因为该写操作已由 JVM 在写操作之后执行。

非常感谢您的回答。我可以认为我的两个理解都是正确的吗?还有两个问题:"w.v = r.v" 是什么意思?左边的数字是什么意思?再次感谢。 - newman
根据我的理解,我发现如果程序正确同步,它仍然允许程序中存在数据竞争。那么你对这个观点有什么看法? - newman
非常感谢。我明天会继续这个话题。再见。 - newman
有两个结论来自JLS: C1:如果一个程序没有数据竞争,那么程序的所有执行看起来都是按顺序一致的。 C2:如果一个程序被正确同步,那么程序的所有执行看起来都是按顺序一致的。 若C1的另一个方向是正确的,则我们得到以下结论: C3:如果一个程序被正确同步,那么就没有数据竞争问题。 但不幸的是,在JLS中没有这样的方向,因此我得出了这个结论: - newman
我觉得重新提起这个话题有点奇怪,但你说“...在程序顺序中写入后读取的内容...”,而“发生在一致性之前”与程序顺序无关。它是规则,指出一个_读取_要么观察到happens-before中的_写入_,要么观察到_任何其他读取_(竞争)。该规则定义了synchronizes-with顺序之间的连接,构建了多个线程之间的happens-before order - Eugene
显示剩余4条评论

1

问题1: 我的理解正确吗?

答案:是的。

问题2: “w.v = r.v”是什么意思?

答案:w.v的值与r.v的值相同。

问题3: 左边的数字表示什么?

答案:我认为它是语句ID,就像“表17.4-A。由语句重新排序引起的令人惊讶的结果-原始代码”中所示。但是您可以忽略它,因为它不适用于“另一种执行顺序,即发生在之前的顺序是:”。所以左边的数字完全无关紧要。不要坚持它。

问题4: 在我的理解中,r2和r1都可以看到0的初始写入的原因是A和B都不是易失性字段。所以我的第四个问题是:我的理解是否正确?

答案:这是一个原因。重新排序也可能会导致这种情况。“必须正确同步程序,以避免在代码重新排序时可能观察到的反直觉行为。”

问题5&6: 在第二个执行顺序中……所以我的第五和第六个问题是:现实世界中存在这种情况(读取后面发生的写入)吗?如果存在,您能给我一个真实的例子吗?

A: 是的。代码中没有同步,每个线程读取时可以看到初始值的写入或其他线程的写入。

时间1:线程2:A=2

时间2:线程1:B=1 // 没有同步,线程1的B=1可能会在此处交错

时间3:线程2:r1=B // r1的值为1

时间4:线程1:r2=A // r2的值为2

注意:“如果一个执行是happens-before一致的,则其动作集是happens-before一致的”


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接