内存不一致与线程交错有何不同?
我正在编写一个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 A
和Thread 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-before
的happens-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可能会做一个多个不同线程之间有很多同步,所以很多内存可能实际上都被刷新了,你不一定能看到你想要的效果……很难模拟这一切!