严格来说,理解 JVM 内存模型最具挑战性的事情之一是:timing(即你的挂钟)完全无关。
不管how long(根据您的挂钟)如果没有,则两个单独线程中的两个操作之间经过的时间发生在之前关系,绝对不能保证每个线程将在内存中看到什么。
In your 实施例2,棘手的部分是你提到的,
虽然线程 A 没有明确发生在线程 B 之前,显然是这样.
从上面的描述来看,你唯一能说的是obvious就是根据你挂钟的时间测量,发生了一些操作later相对于其它的. But 这并不意味着发生在严格意义上的 JVM 内存模型的关系。
让我展示一组与您对上面示例 2 的描述兼容的操作(即,根据您的挂钟进行的测量),这可能会导致true or false,并且不能做出任何保证。
-
主线程M启动线程A和线程B: 有一个发生在关系之前线程 M 和线程 A 之间以及线程 M 和线程 B 之间。因此,如果没有发生其他情况,线程 A 和线程 B 都将看到与线程 M 相同的布尔值。我们假设它被初始化为
false
(以使其与您的描述兼容)。
假设您正在多核计算机上运行。另外,假设线程 A 分配在 Core 1 中,线程 B 分配在 Core 2 中。
线程A读取布尔值: 它必然会读取false
(请参阅前面的要点)。当这个读取发生时,它might某些内存页(包括包含该布尔值的内存页)将被缓存到 Core 1 的 L1 缓存或 L2 缓存(该特定核心本地的任何缓存)中。
线程A取反并存储布尔值: 它将存储true
现在。但问题是:在哪里?直到一个发生在之前发生这种情况时,线程 A 可以自由地将这个新值仅存储在运行该线程的 Core 的本地缓存中。因此,该值可能会在 Core 1 的 L1/L2 缓存中更新,但在处理器的 L3 缓存或 RAM 中保持不变。
经过一些time(根据你的挂钟),线程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 对非易失性变量进行更改,然后更新易失性变量,然后(从挂钟的角度来看)线程 B 读取该易失性变量,也保证线程 B 会看到所有更改到非易失性变量!这是由非常复杂的代码使用的,这些代码想要避免锁并且仍然需要强大的内存一致性语义。它通常被称为挥发性变量捎带.
最后一点,如果您尝试simulate(缺乏)发生在关系之前,这可能会令人沮丧......当您将内容写到控制台时(即,System.out.println
),JVM 可能会在多个不同线程之间进行大量同步,因此大量内存实际上可能会被刷新,并且您不一定能够看到您正在寻找的效果...很难模拟所有这!