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

use*_*804 3 java memory multithreading volatile

我正在编写一个多线程程序,正在研究是否应该使用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
Run Code Online (Sandbox Code Playgroud)

由于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)
Run Code Online (Sandbox Code Playgroud)

我强烈认为示例2不可能是真的.问题是只有在它真的时才能看到volatile用来建立一个happens-before.

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

Bru*_*eis 5

关于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捎带.


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