繁体   English   中英

了解 Java 易失性可见性

[英]Understanding Java volatile visibility

我正在阅读有关 Java volatile关键字的信息,但对其“可见性”感到困惑。

volatile 关键字的典型用法是:

volatile boolean ready = false;
int value = 0;

void publisher() {
    value = 5;
    ready = true;
}

void subscriber() {
    while (!ready) {}
    System.out.println(value);
}

正如大多数教程所解释的,使用 volatile for ready可确保:

  • 在发布者线程上更改为ready对其他线程(订阅者)立即可见;
  • ready的变化是对其他线程可见,前述的任何变量更新ready (这里是value的变化)也是其他线程可见的;

我理解第二点,因为volatile变量通过使用内存屏障来防止内存重新排序,所以在 volatile write 之前的写入不能在它之后重新排序,并且在 volatile read 之后的读取不能在它之前重新排序。 这就是ready防止上面演示中打印value = 0 的方式。

但是我对第一个保证感到困惑,即 volatile 变量本身的可见性。 对我来说,这听起来是一个非常模糊的定义。

换句话说,我的困惑仅在于 SINGLE 变量的可见性,而不是多个变量的重新排序或其他什么。 让我们简化上面的例子:

volatile boolean ready = false;

void publisher() {
    ready = true;
}

void subscriber() {
    while (!ready) {}
}

如果ready没有定义为 volatile,那么订阅者是否有可能无限地卡在 while 循环中? 为什么?

我想问几个问题:

  • “立即可见”是什么意思? 写操作需要一些时间,那么其他线程多久可以看到 volatile 的变化呢? 在写入开始后不久但在写入完成之前发生的另一个线程中的读取是否可以看到更改?
  • 可见性,对于现代 CPU 来说,缓存一致性协议(例如 MESI)无论如何都可以保证,那么volatile在这里有什么帮助呢?
  • 有些文章说 volatile 变量直接使用内存而不是 CPU 缓存,这保证了线程之间的可见性。 这听起来不是一个正确的解释。
   Time : ---------------------------------------------------------->

 writer : --------- | write | -----------------------
reader1 : ------------- | read | -------------------- can I see the change?
reader2 : --------------------| read | -------------- can I see the change?

希望我清楚地解释了我的问题。

语言规范的相关部分:

volatile 关键字: https : //docs.oracle.com/javase/specs/jls/se16/html/jls-8.html#jls-8.3.1.4

内存模型: https : //docs.oracle.com/javase/specs/jls/se16/html/jls-17.html#jls-17.4

正如您所说,CPU 缓存不是这里的一个因素。

这更多是关于优化。 如果ready不是 volatile,则编译器可以自由解释

// this
while (!ready) {}

// as this
if (!ready) while(true) {}

这当然是一种优化,它必须更少地评估条件。 该值在循环中不会改变,可以“重用”。 就单线程语义而言,它是等效的,但它不会做你想要的。

这并不是说这会一直发生。 编译器可以自由地这样做,他们不必这样做。

可见性,对于现代 CPU 来说,缓存一致性协议(例如 MESI)无论如何都可以保证,那么 volatile 在这里有什么帮助呢?

那对你没有帮助。 您不是在为现代 CPU 编写代码,而是在为 Java 虚拟机编写代码,该虚拟机允许具有虚拟 CPU 的虚拟机的虚拟 CPU 缓存不一致。

有些文章说 volatile 变量直接使用内存而不是 CPU 缓存,这保证了线程之间的可见性。 这听起来不是一个正确的解释。

那是正确的。 但是请理解,这与您正在编码的虚拟机有关。 它的内存很可能在您的物理 CPU 的缓存中实现。 这可能允许您的机器使用缓存并仍然具有 Java 规范所需的内存可见性。

使用volatile可以确保写入直接进入虚拟机的内存,而不是虚拟机的虚拟 CPU 缓存。 虚拟机的 CPU 缓存不需要提供线程之间的可见性,因为 Java 规范不需要它。

您不能假设您的特定物理硬件的特性一定会提供 Java 代码可以直接使用的好处。 相反,JVM 会牺牲这些好处来提高性能。 但这意味着您的 Java 代码无法获得这些好处。

同样,您不是为物理 CPU编写代码,而是为 JVM 提供的虚拟 CPU 编写代码。 那你的CPU有连贯的高速缓存允许JVM做各种能够提升你的代码的性能优化,但要求JVM通过对你的代码和真正的JVM的通过那些连贯的缓存没有。 这样做将意味着消除大量极其有价值的优化。

如果 ready 没有定义为 volatile,那么订阅者是否有可能无限地卡在 while 循环中?

是的。

为什么?

因为订阅者可能永远不会看到发布者写入的结果。

因为...... JLS不需要将变量的值写入内存......除非满足指定的可见性约束。

“立即可见”是什么意思? 写操作需要一些时间,那么其他线程多久能看到 volatile 的变化呢? 在写入开始后不久但在写入完成之前发生的另一个线程中的读取是否可以看到更改?

(我认为)JMM 指定或假设在物理上不可能同时读取和写入相同的概念内存单元。 因此,对存储单元的操作是按时间顺序进行的。 立即可见意味着在写入后的下一个可能的读取机会中可见。

可见性,对于现代 CPU 来说,缓存一致性协议(例如 MESI)无论如何都可以保证,那么 volatile 在这里有什么帮助呢?

  1. 编译器通常生成将变量保存在寄存器中的代码,并且仅在必要时将值写入内存。 将变量声明为volatile意味着该值必须写入内存。 如果考虑到这一点,则不能依赖缓存实现的(假设或实际)行为来指定volatile含义。

  2. 虽然当前这一代现代 CPU / 缓存架构的行为方式如此,但并不能保证所有未来的计算机都会如此行为。

有些文章说 volatile 变量直接使用内存而不是 CPU 缓存,这保证了线程之间的可见性。

有人说这是不正确的……对于实现缓存一致性协议的 CPU。 然而,这不是重点,因为如上所述,变量的当前值可能尚未写入缓存。 实际上,它可能永远不会写入缓存。

 Time : ----------------------------------------------------------> writer : --------- | write | ----------------------- reader1 : ------------- | read | -------------------- can I see the change? reader2 : --------------------| read | -------------- can I see the change?

因此,让我们假设您的图表显示物理时间并表示在不同物理内核上运行的线程,通过它们各自的缓存读取和写入缓存一致的内存单元。

在物理层会发生什么取决于缓存一致性是如何实现的。

希望Reader 1 可以看到单元格的先前状态(如果它可以从其缓存中获得)或新状态(如果不是)。 读者 2 将看到新状态。 但这也取决于写入线程的缓存失效传播到其他缓存所需的时间。 以及其他各种难以解释的东西。

简而言之,我们真的不知道在物理层面会发生什么。

但另一方面,上图中的作者和读者无论如何都无法真正观察到物理时间。 程序员也不能。

程序/程序员看到的是读写不重叠。 当必要的发生在关系存在之前,将保证一个线程的内存写入对另一个线程的后续1读取的可见性。 这适用于 volatile 变量,以及其他各种事物。

如何实施保证,不是你的问题。 如果您确实了解它在硬件级别上发生的事情,那真的无济于事,因为您实际上并不知道 JIT 编译器将发出什么代码(今天!)。


1 - 也就是说,根据同步顺序随后......您可以将其视为逻辑时间。 JLS 内存模型实际上根本不谈论时间。

回答你的3个问题:

  1. 易失性写入的更改不需要对易失性负载“立即”可见。 正确同步的 Java 程序将表现得好像它是顺序一致的,并且对于顺序一致性,加载/存储的实时顺序是不相关的。 因此只要不违反程序顺序(或只要没有人可以观察到它),读取和写入就可以倾斜。 线性化 = 顺序一致性 + 尊重实时顺序。 有关更多信息,请参阅此答案

  2. 我仍然需要深入研究可见的确切含义,但 AFAIK 主要是编译器级别的问题,因为硬件将无限期地阻止缓冲加载/存储。

  3. 你是完全正确的文章是错误的。 写了很多废话,“将易失性写入刷新到主内存而不是使用缓存”是我看到的最常见的误解。 我认为我所有的 SO 评论中有 50% 是关于告诉人们缓存总是连贯的。 关于该主题的一本好书是“内存一致性和缓存一致性入门 2e”,可免费获得

Java 内存模型的非正式语义包含 3 个部分:

  • 原子性
  • 能见度
  • 订购

原子性是关于确保读/写/rmw 在全局内存顺序中原子地发生。 所以没有人可以观察到一些介于两者之间的状态。 这涉及访问原子性,如读/写撕裂、字撕裂和正确对齐。 它还处理像 rmw 这样的操作原子性。

恕我直言,它还应该处理存储原子性; 因此,请确保有一个时间点,所有核心都可以看到存储。 例如,如果您有 X86,那么由于负载缓冲,存储可以比其他内核更早地对发布内核可见,并且您违反了原子性。 但我还没有看到 JMM 中提到它。

可见性:这主要是为了防止编译器优化,因为硬件将无限期地防止延迟加载和缓冲存储。 在一些文献中,他们还会在可见性下对周围的负载/商店进行排序; 但我不相信这是正确的。

排序:这是内存模型的基础。 它将确保单个处理器发出的加载/存储不会被重新排序。 在第一个示例中,您可以看到这种行为的必要性。 这是编译器屏障和cpu内存屏障的领域。

有关更多信息,请参阅: https : //download.oracle.com/otndocs/jcp/memory_model-1.0-pfd-spec-oth-JSpec/

我只谈这部分:

在发布者线程上更改为就绪对其他线程立即可见

这是不正确的,文章是错误的。 文档在这里做了一个非常明确的声明:

对 volatile 字段的写入发生在对该字段的每次后续读取之前。

这里比较复杂的部分是后续 通俗地说,这意味着当有人将ready视为true ,它也会将value视为5 这自然意味着您需要观察该值是否为true ,并且您可能会观察到不同的事情。 所以这不是“立即”。

人们对此感到困惑的是, volatile提供了顺序一致性,这意味着如果有人观察到ready == true ,那么每个人也会(例如,与release/acquire不同)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM