java中的volatile关键字真的与缓存有关吗?

Jak*_*ake 1 java caching volatile

从我读过的内容来看,java 中的“volatile”关键字确保线程始终获取特定指针的最新值,通常通过直接从/向内存读取/写入以避免缓存不一致。

但为什么需要这样做呢?据我所知,这已经在硬件级别上完成了。如果我从我的系统架构类中没有记错的话,更新内存位置的处理器核心会向其他处理器的缓存发送无效信号,迫使它们在需要时从内存中获取这些行。或者,如果情况相反——如果处理器获取内存,它将强制其他缓存的缓存(但未写入)行首先刷新到内存中。

我唯一的理论是,尽管我已经阅读了所有解释,但这实际上与缓存完全无关。它与 JVM 中的数据可以驻留在两个地方有关 - 线程的本地堆栈和堆。并且 Java 线程可以将其堆栈用作一种缓存。我会买那个,但这也意味着对驻留在堆上的数据使用 volatile 是无用的,因为它由所有线程共享并遵守硬件实现的一致性?

例如:

public final int[] is = new int[10];
Run Code Online (Sandbox Code Playgroud)

访问 is 的数据将始终导致获得最新的数据,因为数据驻留在堆上。然而,指针是一个原语,可能会成为堆栈问题的受害者,但由于它是最终的,我们没有这个问题。

我的假设正确吗?

编辑:据我所知,这不是重复的。所谓的重复线程是那些误导性的答案之一,它说它与缓存一致性有关。我的问题不是 volatile 用于什么,也不是如何使用它。它正在测试一种理论,并且进行更深入的测试。

Ste*_*n C 5

但为什么需要这样做呢?据我所知,这已经在硬件级别上完成了。

这是不正确的。在现代多核系统中,简单的内存写入指令不一定会写入主内存(当然,不是立即),并且不保证内存读取指令从主内存/其他缓存读取最新值。如果内存读/写指令总是这样做,内存缓存将是浪费时间。

为了保证这些事情,(本机代码)编译器需要在导致缓存写入或缓存失效的关键点发出指令。

我唯一的理论是,这实际上与缓存完全无关......

那是不正确的。这一切都与缓存有关。问题是您误解了典型现代多核处理器上的典型指令如何处理缓存。

ISA 的设计使缓存使单线程代码运行得更快……通过避免进入主内存。如果只有一个线程在给定地址读取和写入值,那么处理器缓存中值的副本比主内存中的副本新这一事实无关紧要。

但是当有多个线程时,您可以在不同的内核上运行两个线程,并使用不同的内存缓存。如果有 N 个内核,则给定地址->值关联可能有 N+1 个不同的“版本”。那是混乱的。在 Java 中有两种方法可以解决这个问题:

  • 将变量声明为volatile告诉编译器使用(昂贵的)缓存刷新和/或缓存失效指令(或作为副作用刷新或失效的指令)来实现读取和写入。

  • 使用适当的同步,并依靠发生之前的关系通知编译器在哪里放置内存屏障。典型的同步操作还将涉及根据需要刷新和/或使缓存无效的指令。这通常是提供内存屏障的原因。

但是,这都是特定于 ISA 的,所使用的特定机器指令将取决于 JIT 编译器。


参考:


另一件需要注意的是,在典型的编译程序中还有另一种缓存:在寄存器中缓存临时变量。

Java 内存模型没有直接讨论硬件的行为:内存缓存和寄存器。相反,它指定了保证多线程应用程序使用共享内存的情况。但其根本原因远比“文字撕裂”更广泛。

最后,JVM 抽象机中的“表达式堆栈”实际上只是用于指定操作语义的一种技巧。当字节码被编译为本机代码时,值存储在硬件机器寄存器或硬件内存位置。表达式堆栈不再存在。当然,调用堆栈/本地帧是存在的。但它们是作为普通内存实现的。