Volatile 关键字和 CPU 缓存一致性协议

Har*_*Lee 1 java cpu caching volatile cpu-cache

CPU 已经通过一些协议(如 MESI)保证缓存一致性。为什么我们还需要volatile在某些语言(如java)中保持多线程之间的可见性。

可能的原因是这些协议在启动时未启用,并且必须由某些指令(例如LOCK.

如果确实如此,为什么CPU在启动时不启用该协议?

pve*_*jer 5

Volatile 可防止 3 种不同类型的问题:

  • 能见度
  • 重新排序
  • 原子性

我假设是X86..

首先,X86 上的缓存始终是一致的。因此,不会发生这样的情况:一个 CPU 将某个变量的存储提交到缓存后,另一个 CPU 仍会加载该变量的旧值。这是 MESI 协议的领域。

假设 Java 字节码中的每个 put 和 get 都被转换(并且没有优化)到 CPU 上的存储和加载,那么即使没有 易失性,每个 get 也会看到对同一地址的最新 put。

这里的问题是编译器(在本例中为 JIT)有很大的自由度来优化代码。例如,如果它检测到在循环中读取相同的字段,它可能会决定将该变量提升到循环之外,如下所示。

 for(...){
       int tmp = a;
       println(tmp);
 }
Run Code Online (Sandbox Code Playgroud)

吊装后:

 int tmp = a;
 for(...){
       println(tmp);
 }
Run Code Online (Sandbox Code Playgroud)

如果该字段仅被 1 个线程触及,那么这很好。但如果该字段被另一个线程更新,第一个线程将永远不会看到更改。使用 volatile 可以防止此类可见性问题,这实际上是以下行为:

  • C 风格易变
  • 在 JSR-133 引入 Java 内存模型之前,Java 易失性。
  • 具有不透明访问模式的 VarHandle。

那么 volatile 还有一个非常重要的方面;易失性可防止某些 CPU 执行的指令流中不同地址的加载和存储被重新排序。JIT 编译器和 CPU 有很大的自由度来重新排序加载和存储。尽管在 X86 上,由于存储缓冲区的原因,只能将较旧的存储与较新的加载重新排序到不同的地址。

想象一下下面的代码:

int a;
volatile int b;

thread1:
    a=1;
    b=1;

thread2:
    if(b==1) print(a);
Run Code Online (Sandbox Code Playgroud)

易失性的事实b阻止了 storea=1跳转到 store 之后b=1。并且它还可以防止 的负载a在 的负载之前跳入b。这样线程 2 就可以保证a=1在读取时看到b=1

所以使用 volatile,可以保证非 volatile 字段对其他线程可见。

如果您想了解 易失性 是如何工作的,我建议深入研究 Java 内存模型,该模型以同步和发生之前规则表示,正如 Margeret Bloom 已经指出的那样。我已经给出了一些低级细节,但对于 Java,最好使用这个高级模型,而不是从硬件角度考虑。只考虑硬件/围栏只适合专家,不可移植且非常脆弱。

  • “_X86 上的缓存始终是一致的_”不仅在 Intel 兼容的 CPU 上,而且在使用任何高级语言(如 Java 或 C++)进行 MT 编程时可能在任何地方使用的任何 CPU 上也是如此。 (2认同)