15

JLS 的第 17 章中,引入了一个概念:happens-before 一致。

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

在我的理解中,它等于下面的话:……,既不是……也不是……

所以我的前两个问题是:

  • 我的理解正确吗?
  • “wv = rv”是什么意思?

它还给出了一个例子: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 都不是 volatile 字段。所以我的第四个问题是:我的理解是否正确?

在第二个执行顺序中:

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

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

2: B = 1;

4: A = 2;

根据happens-before一致性的定义,不难理解这个执行顺序是happens-before一致(如果我的第一个理解是正确的)。所以我的第五个和第六个问题是:现实世界中是否存在这种情况(读后写)?如果是这样,你能给我一个真实的例子吗?

4

5 回答 5

18

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

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

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

在普通 CPU 上,缓存是“连贯的”(不能保存陈旧/冲突的值)并且是透明的,而不是手动管理的。使数据在线程之间可见仅意味着在 asm 中执行实际的加载或存储指令以访问内存(通过数据缓存),并可选地等待存储缓冲区耗尽以给出排序 wrt。其他后期操作。

于 2012-08-15T13:55:21.100 回答
6

发生之前

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

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

可见性- 如果一个线程进行了更改,它们对其他线程是可见的。volatile在 Java 5 之前happens-before

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

例如happens-before,这是一种memory barrier有助于解决问题的Visibility方法。Ordering发生之前的好例子是volatile[About]synchronized监控[About]

一个很好的例子atomicityCompare and swap( ) 模式的( CAS) 实现,它应该是原子的并且允许在多线程环境中更改变量。如果满足以下条件,您可以编写自己的实现:check then actCTACTA

  • volatile + synchronized
  • java.util.concurrent.atomic使用sun.misc.Unsafe(内存分配,没有构造函数调用的实例化......)从中Java 5使用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

发生之前

官方文档

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

在此处输入图像描述

以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
}

可见性- 当Thread A 更改/写入失性变量时,它还会将所有先前的更改推送到RAM - 主内存因此所有非易失性变量都将是最新的并且对其他线程可见

订购

  • 写入 volatile 变量 in 之前的Thread A所有操作都将在之前调用。JVM 能够对它们重新排序,但保证在写入 volatile 变量之前Thread A不会调用任何操作。

  • 读取 volatile 变量后的Thread B所有操作都将在之后调用。JVM 能够对它们重新排序,但保证在读取 volatile 变量后Thread B不会调用任何操作。

[并发与并行]

于 2019-12-26T16:41:59.903 回答
4

Java 内存模型定义了程序中所有操作的部分排序,称为发生前
为了保证线程Y能够看到动作的副作用X(与是否X发生在不同的线程中无关),在和之间定义了发生前的关系。 如果这种关系不存在,JVM 可能会重新排序程序的操作。 现在,如果一个变量被多个线程共享和访问,并且(至少)一个线程写入,如果读取和写入不是按照发生前的关系排序的,那么你就有了数据竞争。XY


在正确的程序中没有数据竞争。
示例是 2 个线程AB在 lock 上同步X
Thread A获取锁(现在Thread B被阻塞)并执行写操作,然后释放锁X。现在Thread B获取锁X,并且由于所有操作Thread A都是在释放锁之前完成的X,因此它们在线程之后获取锁的操作之前排序(并且对 也是可见的)。 请注意,这发生在同一个锁上同步的操作上。在线程之间的关系同步之前没有发生Thread BX AThread B
在不同的锁上

于 2012-08-15T14:05:18.343 回答
2

实质上是正确的。主要的一点是:除非您使用某种形式的同步,否则无法保证在您的程序顺序中写入之后的读取会看到该写入的效果,因为语句可能已被重新排序。

在现实世界中是否存在这种情况(读取看到稍后发生的写入)?如果是这样,你能给我一个真实的例子吗?

从挂钟的角度来看,显然,读取无法看到尚未发生的写入的效果。

从程序顺序的角度来看,因为如果没有适当的同步(发生在关系之前),语句可以重新排序,在程序中写入之前的读取可以在执行期间看到写入的效果,因为它已经执行在JVM写入之后。

于 2012-08-15T14:33:53.323 回答
1

Q1:我的理解对吗?

答:是的

Q2:“wv = rv”是什么意思?

A: wv 的值与 rv 的值相同

Q3:左数是什么意思?

A:我认为是“表 17.4-A。语句重新排序导致的令人惊讶的结果 - 原始代码”中所示的语句 ID。但是你可以忽略它,因为它不适用于“Another execution order that is occurred-before compatible is:”的内容,所以左边的数字完全是狗屎。不要坚持下去。

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

答:这是一个原因。重新订购也可以。“程序必须正确同步,以避免在重新排序代码时出现违反直觉的行为。”

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

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

时间 1:线程 2:A=2

time 2: Thread 1: B=1 // 不同步,线程1的B=1可以在这里交错

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

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

注意“如果其动作集发生之前一致,则执行是发生之前一致”

于 2021-04-11T21:06:08.857 回答