【问题标题】:Using Java volatile keyword in non-thread scenario在非线程场景中使用 Java volatile 关键字
【发布时间】:2022-01-23 11:22:53
【问题描述】:

我了解 Java 关键字 volatile 用于多线程上下文;主要目的是从内存中读取而不是从缓存中读取,或者即使从缓存中读取,也会先更新。

在下面的例子中,没有多线程的概念。我想了解变量 i 是否会作为代码优化的一部分被缓存,从而从 cpu 缓存而不是内存中读取?如果是的话,如果变量声明为volatile,肯定会从内存中读取吗?

我通过添加和删除 volatile 关键字多次运行该程序;但是,由于 for 循环没有固定的时间,因此当变量声明为 volatile 时是否消耗更多时间,我无法得出结论。

我只想看到,从 CPU 缓存中花费的时间实际上比它声明为 volatile 时要少。

我的理解是否正确?如果是,我怎样才能看到这个概念在工作中,并很好地记录了 CPU 缓存读取和内存读取的时间?

import java.time.Duration;
import java.time.Instant;

public class Test {
    
    volatile static int i=0;
//  static int i=0;

    public static void main(String[] args) {
        Instant start = Instant.now();

        for (i=0; i<838_860_8; i++) { // 2 power 23; ~ 1 MB CPU Cache
            System.out.println("i:" + i);
        }
        Instant end  = Instant.now();
        
        long timeElapsed = Duration.between(start, end).getSeconds();
        System.out.println("timeElapsed: " + timeElapsed + " seconds.");

    }
}

【问题讨论】:

  • 不确定你的基准,没有重大变化,测试很多。你确定 JVM 已经离开解释模式了吗?不相信您的代码甚至会被 JIT 化
  • 是的,该变量将从内存中读取,因为 volatile 保证 JVM 从内存中读取它。至于基准测试是@Bor
  • @BoristheSpider 我只是假设变量 i 将存储在 CPU 缓存中,主要是因为在任何给定时间点,它的值都是
  • 变量中存储的值与它是否存储在缓存或主内存中无关。您可能正在考虑一个大型数组,它不能作为一个整体放入缓存中,但现在您只有一个 int 变量,它只占用 4 个字节的内存。
  • 按逻辑,如果使用volatile,肯定有一些缺点,否则就是默认的,甚至不存在。通过 JMH 测试,差异很大(循环系数为 20 并对变量 {println not used} 求和)

标签: java caching volatile


【解决方案1】:

我认为答案是“可能是的”......对于当前的 Java 实现。

我们无法确定的原因有两个。

  1. Java 语言规范实际上并没有说明寄存器、CPU 缓存或类似的东西。它实际上说的是在一个写 volatile 的线程和另一个线程(随后)读取它之间存在 happens before 关系。

  2. 虽然可以合理地假设这会在有多个线程的情况下影响缓存,但如果 JIT 编译器能够推断出 volatile 变量对于给定的线程线程受限执行您的应用程序时,可能会导致它可以缓存变量。


这就是理论。

如果存在可衡量的性能差异,您将能够在正确编写的基准测试中对其进行衡量。 (虽然根据 Java 版本和您的硬件,您可能会得到不同的结果。)

但是,您的基准测试的当前版本有许多缺陷,这会使它给出的任何结果都令人怀疑。如果您想获得有意义的结果,我强烈建议您阅读以下问答。

(不幸的是,某些答案中的某些链接似乎已损坏...)

【讨论】:

    【解决方案2】:

    你的基准测试的前提是有缺陷的;缓存是事实的来源。缓存一致性协议确保 CPU 缓存是一致的;内存只是不适合缓存的任何内容的溢出桶,因为大多数缓存都是后写(而不是直写)。换一种说法;不需要将 volatile 写入主存储器;写入缓存就足够了。

    一些不需要写入缓存的示例:

    • I/O DMA:您希望防止写入缓存,否则主内存和 CPU 缓存可能会变得不连贯。
    • 非时间数据:例如您正在处理一些庞大的数据集,并且您只访问它一次,缓存它是没有意义的。

    但这超出了常规 Java volatile 的范围。

    使用 volatile 是要付出代价的:

    1. 原子性保证。
    2. 无法优化加载和存储。这排除了许多编译器优化。 Volatile 不会阻止使用 CPU 寄存器;它只会“无限期地”防止“缓存”寄存器中的变量内容。
    3. 以栅栏的形式订购保证。在上述案例中的 X86 上,价格处于 volatile store。存储需要等待存储和所有更早的存储提交到缓存。

    尤其是最后两个会影响不稳定的性能。

    除此之外,您的基准测试存在缺陷。首先,正如其他人已经指出的那样,我会切换到 JMH。它将处理相当多的典型基准测试错误,例如预热和死代码消除。此外,您不应该在基准测试中使用 System.out,因为这将完全影响性能。

    【讨论】: