Java 易失性是否阻止缓存或强制实施直写缓存?

我试图理解Java的关键字,即在具有CPU缓存的多线程程序中写入易失性原子变量。volatile

我已经阅读了几个教程和Java语言规范,特别是关于“在排序之前发生”的第17.4.5节。我的理解是,当线程将新值写入易失性变量时,更新后的值必须对读取该变量的其他线程可见。对我来说,这些语义可以通过以下两种方式之一实现:

  1. 线程可以在 CPU 缓存中缓存易失性变量,但对缓存中变量的写入必须立即刷新到主内存。换句话说,缓存是直写的

  2. 线程永远无法缓存易失性变量,并且必须在主内存中读取和写入此类变量。

本教程http://tutorials.jenkov.com)中提到了方法 1,它说:

通过声明计数器变量易失性,对计数器变量的所有写入都将立即写回主内存。

方法 2 在 Stackoverflow 问题“Java 中的易失性变量”中提到,本教程还说:

此变量的值永远不会在本地线程缓存:所有读取和写入都将直接进入“主内存”

哪一种是Java中使用的正确方法?

相关的堆栈溢出问题没有回答我的问题:

Java 中的易失性变量

Java 易失性读刷新写入和易失性写更新读取

Java 易失性和高速缓存一致性


答案 1

保证只是您在语言规范中看到的内容。从理论上讲,编写易失性变量可能会强制缓存刷新到主内存,或者可能不会,也许后续读取会强制缓存刷新,或者以某种方式导致在没有缓存刷新的情况下在缓存之间传输数据。这种模糊性是故意的,因为它允许潜在的未来优化,如果更详细地阐明易失性变量的机制,这些优化可能是不可能的。

在实践中,对于当前的硬件,这可能意味着,如果没有一致的缓存,写入易失性变量会强制缓存刷新到主内存。当然,使用一致的缓存,不需要这样的刷新。


答案 2

在Java中,最准确地说,所有线程都将看到最近对易失性字段的写入,以及该易失性读/写之前的任何写入。

在Java抽象中,这在功能上等同于从共享内存中读取/写入的易失性字段(但这在较低级别上并不严格准确)。


在比与Java相关的级别低得多的水平上;在现代硬件中,对任何和所有内存地址的任何和所有读取/写入始终发生在L1中,并且首先寄存器。话虽如此,Java旨在向程序员隐藏这种低级行为,因此这仅在概念上与讨论相关。

当我们在Java中的字段上使用关键字时,这只会告诉编译器在读取/写入该字段时插入称为内存屏障的东西。记忆屏障有效地确保了两件事;volatile

  1. 读取此地址的任何线程都将使用最新的值(屏障使它们等到最近的写入操作返回到共享内存,并且在此更新的值进入其 L1 高速缓存之前,任何读取线程都不能继续)。

  2. 对任何字段的读取/写入都不能越过障碍(也就是说,它们总是在另一个线程可以继续之前被写回去,编译器/ OOO不能将它们移动到障碍之后的某个点)。

举一个简单的Java例子;

//on one thread
counter += 1; //normal int field
flag = true; //flag is volatile

//on another thread
if (flag) foo(counter); //will see the incremented value

从本质上讲,当设置为 时,我们会创建一个内存屏障。当线程 #2 尝试读取此字段时,它会遇到我们的障碍并等待新值到达。同时,CPU 确保在新值到达之前将其写回。结果,如果然后将被增加。flagtruecounter += 1flag == truecounter


所以总结一下;

  1. 所有线程都会看到易失性字段的最新值(可以粗略地描述为“读取/写入通过共享内存”)。

  2. 对易失性字段的读取/写入与以前对一个线程上任何字段的读取/写入建立发生之前的关系。