内存不一致与线程交错有何不同?
How is memory inconsistency different from thread interleaving?
我正在编写一个多线程程序,正在研究是否应该使用 volatile
作为我的布尔标志。 docs oracle trail on concurrency 没有解释任何关于 memory consistency errors
除了:
Memory consistency errors occur when different threads have
inconsistent views of what should be the same data.
假设这些 不一致的视图 只发生在 'write' 操作之后是有道理的。但是多久之后呢?
示例 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
同时运行,打印结果也可能为真,这取决于它检索 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 不可能成立。问题是只有当为 true 时,我才能看到使用volatile
来建立happens-before
.
如果是真的,为什么会这样?如果不是.. 为什么要使用 volatile
呢?
理解 JVM 内存模型最具挑战性的事情之一是,严格来说,计时(即您的挂钟)完全无关紧要。
无论多长时间(根据您的挂钟)时间在 2 个独立线程中的 2 个操作之间流逝,如果没有 happens-before 关系,绝对不能保证每个线程在内存中看到什么。
在您的 示例 2 中,棘手的部分是您提到的
while Thread A didn't explicitly happen-before Thread B, it obviously did.
根据上面的描述,您唯一可以说的是显而易见,根据您挂钟的时间测量,发生了一些操作比别人晚。但是 这并不意味着 JVM 内存模型严格意义上的 happens-before 关系。
让我展示一组与您对上面示例 2 的描述相符的操作(即,根据您的挂钟进行的测量)并且可能导致 true 或 false,并且无法做出任何保证。
- 主线程M启动了线程A和线程B:有happens-before关系 在线程 M 和线程 A 之间,以及线程 M 和线程 B 之间。因此,如果没有其他事情发生,线程 A 和线程 B 将看到与线程 M 相同的布尔值。假设它被初始化为
false
(以使其与您的描述兼容)。
假设您 运行 在多核机器上。另外,假设线程 A 分配在核心 1 中,线程 B 分配在核心 2 中。
线程 A 读取布尔值:它必然会读取 false
(参见前面的要点)。发生此读取时,可能 会发生一些内存页,包括包含该布尔值的内存页,将被缓存到 Core 1 的 L1 缓存或 L2 缓存中——任何特定的本地缓存核心.
线程A否定并存储布尔值:它现在将存储true
。但问题是:在哪里?在 happens-before 发生之前,线程 A 可以自由地将这个新值仅存储在本地缓存到核心 运行 线程上。因此,该值可能会在 Core 1 的 L1/L2 缓存中更新,但在处理器的 L3 缓存或 RAM 中将保持不变。
经过一些时间(根据你的挂钟),线程B读取布尔值:如果线程A 没有将更改刷新到 L3 或 RAM 中,线程 B 完全有可能读取 false
。另一方面,如果线程 A 刷新了更改,则线程 B 有可能 读取 true
(但仍然不能保证——线程 B 可能收到了一个线程 M 的内存视图的副本,并且由于缺少 happens-before,它不会再次进入 RAM,仍然会看到原始值)。
保证任何事情的唯一方法是明确发生在之前:它会强制线程 A 刷新其内存,并会强制线程 B 不从本地缓存读取,而是真正从 "authoritative" 源读取它。
如果没有 happens-before,正如您在上面的示例中看到的那样,任何事情都可能发生,无论多少时间(从您的角度来看)不同线程中的事件之间经过.
现在,最大的问题是:为什么 volatile
可以解决示例 2 中的问题?
如果该布尔变量被标记为 volatile
,并且如果根据上面的示例 2(即,从挂钟的角度来看)操作交错发生,那么,只有这样,线程 B 才能得到保证看到true
(即,否则根本没有保证)。
原因是 volatile
有助于建立事前发生关系。它是这样进行的:对volatile
变量的写入发生在对同一变量的任何后续读取之前。
因此,通过标记变量volatile
,如果从时序的角度来看线程B只在线程A更新后读取,那么线程B可以保证看到更新(从内存一致性的角度)。
现在有一个非常有趣的事实:如果线程 A 对非易失性变量进行了更改,然后更新了一个易失性变量,然后稍后(从挂钟的角度来看)线程 B 读取了该易失性变量,这也保证了线程 B将看到对非易失性变量的所有更改!这被非常复杂的代码使用,这些代码想要避免锁定并且仍然需要强大的内存一致性语义。它通常被称为 volatile variable piggybacking.
作为最后的评论,如果您尝试模拟(缺乏)happens-before 关系,可能会令人沮丧...当您将内容写到控制台时(即 System.out.println
),JVM 可能会在多个不同线程之间进行大量同步,因此实际上可能会刷新大量内存,并且您不一定能够看到您正在寻找的效果。 .. 这一切都很难模拟!
我正在编写一个多线程程序,正在研究是否应该使用 volatile
作为我的布尔标志。 docs oracle trail on concurrency 没有解释任何关于 memory consistency errors
除了:
Memory consistency errors occur when different threads have inconsistent views of what should be the same data.
假设这些 不一致的视图 只发生在 'write' 操作之后是有道理的。但是多久之后呢?
示例 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
同时运行,打印结果也可能为真,这取决于它检索 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 不可能成立。问题是只有当为 true 时,我才能看到使用volatile
来建立happens-before
.
如果是真的,为什么会这样?如果不是.. 为什么要使用 volatile
呢?
理解 JVM 内存模型最具挑战性的事情之一是,严格来说,计时(即您的挂钟)完全无关紧要。
无论多长时间(根据您的挂钟)时间在 2 个独立线程中的 2 个操作之间流逝,如果没有 happens-before 关系,绝对不能保证每个线程在内存中看到什么。
在您的 示例 2 中,棘手的部分是您提到的
while Thread A didn't explicitly happen-before Thread B, it obviously did.
根据上面的描述,您唯一可以说的是显而易见,根据您挂钟的时间测量,发生了一些操作比别人晚。但是 这并不意味着 JVM 内存模型严格意义上的 happens-before 关系。
让我展示一组与您对上面示例 2 的描述相符的操作(即,根据您的挂钟进行的测量)并且可能导致 true 或 false,并且无法做出任何保证。
- 主线程M启动了线程A和线程B:有happens-before关系 在线程 M 和线程 A 之间,以及线程 M 和线程 B 之间。因此,如果没有其他事情发生,线程 A 和线程 B 将看到与线程 M 相同的布尔值。假设它被初始化为
false
(以使其与您的描述兼容)。
假设您 运行 在多核机器上。另外,假设线程 A 分配在核心 1 中,线程 B 分配在核心 2 中。
线程 A 读取布尔值:它必然会读取
false
(参见前面的要点)。发生此读取时,可能 会发生一些内存页,包括包含该布尔值的内存页,将被缓存到 Core 1 的 L1 缓存或 L2 缓存中——任何特定的本地缓存核心.线程A否定并存储布尔值:它现在将存储
true
。但问题是:在哪里?在 happens-before 发生之前,线程 A 可以自由地将这个新值仅存储在本地缓存到核心 运行 线程上。因此,该值可能会在 Core 1 的 L1/L2 缓存中更新,但在处理器的 L3 缓存或 RAM 中将保持不变。经过一些时间(根据你的挂钟),线程B读取布尔值:如果线程A 没有将更改刷新到 L3 或 RAM 中,线程 B 完全有可能读取
false
。另一方面,如果线程 A 刷新了更改,则线程 B 有可能 读取true
(但仍然不能保证——线程 B 可能收到了一个线程 M 的内存视图的副本,并且由于缺少 happens-before,它不会再次进入 RAM,仍然会看到原始值)。
保证任何事情的唯一方法是明确发生在之前:它会强制线程 A 刷新其内存,并会强制线程 B 不从本地缓存读取,而是真正从 "authoritative" 源读取它。
如果没有 happens-before,正如您在上面的示例中看到的那样,任何事情都可能发生,无论多少时间(从您的角度来看)不同线程中的事件之间经过.
现在,最大的问题是:为什么 volatile
可以解决示例 2 中的问题?
如果该布尔变量被标记为 volatile
,并且如果根据上面的示例 2(即,从挂钟的角度来看)操作交错发生,那么,只有这样,线程 B 才能得到保证看到true
(即,否则根本没有保证)。
原因是 volatile
有助于建立事前发生关系。它是这样进行的:对volatile
变量的写入发生在对同一变量的任何后续读取之前。
因此,通过标记变量volatile
,如果从时序的角度来看线程B只在线程A更新后读取,那么线程B可以保证看到更新(从内存一致性的角度)。
现在有一个非常有趣的事实:如果线程 A 对非易失性变量进行了更改,然后更新了一个易失性变量,然后稍后(从挂钟的角度来看)线程 B 读取了该易失性变量,这也保证了线程 B将看到对非易失性变量的所有更改!这被非常复杂的代码使用,这些代码想要避免锁定并且仍然需要强大的内存一致性语义。它通常被称为 volatile variable piggybacking.
作为最后的评论,如果您尝试模拟(缺乏)happens-before 关系,可能会令人沮丧...当您将内容写到控制台时(即 System.out.println
),JVM 可能会在多个不同线程之间进行大量同步,因此实际上可能会刷新大量内存,并且您不一定能够看到您正在寻找的效果。 .. 这一切都很难模拟!