了解 Java 易失性可见性

han*_*uan 6 java multithreading caching volatile

我正在阅读有关 Javavolatile关键字的内容,并对它的“可见性”感到困惑。

volatile 关键字的典型用法是:

volatile boolean ready = false;
int value = 0;

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

void subscriber() {
    while (!ready) {}
    System.out.println(value);
}
Run Code Online (Sandbox Code Playgroud)

正如大多数教程所解释的,使用 volatileready确保:

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

我理解第二个,因为volatile变量通过使用内存屏障来防止内存重新排序,所以在易失性写入之前的写入不能在其之后重新排序,并且在易失性读取之后的读取不能在其之前重新排序。这就是上面演示中ready防止 print = 0 的方法。value

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

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

volatile boolean ready = false;

void publisher() {
    ready = true;
}

void subscriber() {
    while (!ready) {}
}
Run Code Online (Sandbox Code Playgroud)

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

我想问几个问题:

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

 writer : --------- | write | -----------------------
reader1 : ------------- | read | -------------------- can I see the change?
reader2 : --------------------| read | -------------- can I see the change?
Run Code Online (Sandbox Code Playgroud)

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

Dav*_*rtz 5

对于现代 CPU 来说,可见性是由缓存一致性协议(例如 MESI)保证的,那么 volatile 在这里能提供什么帮助呢?

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

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

那是对的。但请理解,这是相对于您正在编码的虚拟机而言的。它的内存很可能在物理 CPU 的缓存中实现。这可能允许您的计算机使用缓存,并且仍然具有 Java 规范所需的内存可见性。

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

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

同样,您不是为物理 CPU 编写代码,而是为 JVM 提供的虚拟 CPU 编写代码。您的 CPU 具有一致的缓存,允许 JVM 进行各种优化来提高代码的性能,但 JVM 不需要这些一致的缓存传递给您的代码,而真正的 JVM 则不需要。这样做意味着消除大量极其有价值的优化。

  • @pveentjer 当您编写 Java 代码时,您是为在“物理”机器上实现的“虚拟”机器编写的。典型的 JVM 使用物理机的寄存器来实现虚拟机的某些缓存。OP 感到困惑,因为他正在阅读有关虚拟机的信息并认为它适用于物理机。你似乎也在做同样的事情。如果您编写软件来实现虚拟 CPU,您将使用物理 CPU(包括寄存器)的功能来实现该虚拟 CPU 的组件(包括高速缓存)。 (3认同)
  • @pveentjer 你从来没有听说过 JVM 使用物理 CPU 的寄存器作为缓存来避免内存访问? (2认同)