内存不一致与线程交错有何不同?

我正在编写一个multithreading程序,正在研究是否应该使用volatile作为我的布尔标志。 关于并发性的文档oracle跟踪并没有解释除了以下内容之外的任何memory consistency errors

当不同的线程具有应该是相同数据的不一致视图时,会发生内存一致性错误。

假设这些不一致的视图仅在“写入”操作之后发生是有意义的。 但是多久之后呢?

例1

 Thread A: Retrieve flag. Thread B: Retrieve flag. Thread A: Negate retrieved value; result is true. Thread A: Store result in flag; flag is now true. Thread B: System.out.print(flag) --> false 

由于Thread AThread B同时运行,因此打印也可以为true,具体取决于何时检索flag 。 对于不一致而言,这是完全合理的。

但是描述memory consistency errors的方式(写入变量不一定反映在其他线程中)听起来这也是如此:

例2

 Thread A: Retrieve flag. Thread A: Change retrieved value; result is true. Thread A: Store result in flag; flag is now true. //any longer amount of time passes (while Thread A didn't explicitly happen-before Thread B, it obviously did.) Thread B: Retrieve flag. Thread B: System.out.print(flag) --> true OR false (unpredictable) 

我强烈认为示例2不可能是真的。 问题是,只有当它真的,我才能看到使用volatile建立一个先happens-beforehappens-before

如果是真的,为什么会这样呢? 如果不是..为什么要使用volatile呢?

关于JVM内存模型最难以理解的一点是,严格来说, 时序 (即你的挂钟)是完全无关紧要的。

无论多长时间 (根据您的挂钟)在2个独立线程中的2次操作之间经过了多长时间 ,如果没有发生在之前的关系,则绝对不能保证每个线程在内存中看到的内容。


在您的示例2中 ,您提到的棘手部分是

虽然线程A没有明确发生 – 在线程B之前, 它显然已经发生了

从上面的描述中,你可以说唯一明显的是,根据你的挂钟测量的时间, 一些操作比其他操作发生得 。 但这并不意味着严格意义上的JVM内存模型之前发生过关系。


让我展示一组与您上面的示例2的描述兼容的操作(即,根据您的挂钟所做的测量),这可能导致 ,并且不能保证。

  • 主线程M启动线程A和线程B :线程M和线程A之间以及线程M和线程B之间存在先发生关系 。因此,如果没有其他事情发生,线程A和线程B都将看到与布尔值的线程M相同的值。 我们假设它被初始化为false (以使其与您的描述兼容)。

假设您在多核计算机上运行。 此外,假设线程A在Core 1中分配,而线程B在Core 2中分配。

  • 线程A读取布尔值 :它必须读为false (参见前面的项目符号点)。 当这种读取发生时, 可能会发生某些内存页面(包括包含该布尔值的内存页面)将被缓存到Core 1的L1缓存或L2缓存中 – 该特定内核的本地缓存。

  • 线程A否定并存储布尔值 :它现在将存储为true 。 但问题是:在哪里? 在发生之前发生之前 ,线程A可以自由地将此新值存储在运行该线程的Core的本地缓存中。 因此,该值可能会在Core 1的L1 / L2高速缓存上更新,但在处理器的L3高速缓存或RAM中保持不变。

  • 一段时间后 (根据您的挂钟),线程B读取布尔值 :如果线程A没有将更改刷新到L3或RAM,则完全有可能线程B将读取false 。 另一方面,如果线程A刷新了更改,则线程B 可能会读取为true (但仍然无法保证 – 线程B可能已收到线程M的内存视图副本,并且由于缺少发生之前,它不会再次进入RAM并仍然会看到原始值。

保证任何事情的唯一方法是在之前有一个明确的发生 :它会强制线程A刷新它的内存,并强制线程B不从本地缓存读取,而是真正从“权威”源读取它。

如果没有事先发生,正如您可以从上面的示例中看到的那样,无论在不同线程中的事件之间经过了多长时间 (从您的角度来看),任何事情都可能发生。


现在,一个大问题: 为什么volatile解决你的例2中的问题

如果该布尔变量被标记为volatile ,并且如果根据上面的示例2(即,从挂钟的角度)发生操作的交错,那么,只有这样,线程B才能保证看到true (即,否则存在根本没有保证。

原因是, volatile有助于建立先发生过的关系。 它如下所示: 在对同一变量的任何后续读取之前发生对volatile变量的写入

因此,通过标记变量volatile ,如果从定时角度看,线程B仅在线程A更新之后读取,则线程B保证看到更新(从内存一致性的角度来看)。

现在有一个非常有趣的事实:如果线程A对非易失性变量进行更改,则更新volatile变量,然后(从挂钟角度来看)线程B读取该volatile变量,同时保证线程B将看到所有更改到非易失性变量! 这是由非常复杂的代码使用,它希望避免锁定并仍然需要强大的内存一致性语义。 它通常称为volatile volatile piggybacking


作为最后的评论,如果你试图模拟 (缺乏)发生在之前的关系,它可能会令人沮丧……当你把东西写到控制台(即System.out.println )时,JVM可能会做一个多个不同线程之间有很多同步,所以很多内存可能实际上都被刷新了,你不一定能看到你想要的效果……很难模拟这一切!