【问题标题】:Increased cost of a volatile write over a nonvolatile write与非易失性写入相比,易失性写入的成本增加
【发布时间】:2014-05-07 17:57:36
【问题描述】:

我一直在阅读有关 volatile (https://www.ibm.com/developerworks/java/library/j-jtp06197/) 的文章,并发现有人说 volatile 写入比非易失性写入要昂贵得多。

鉴于 volatile 是一种同步方式,我可以理解与 volatile 写入相关的成本会增加,但我想知道 volatile 写入究竟是如何比非易失性写入昂贵得多的;是否可能与进行 volatile 写入时跨不同线程堆栈的可见性有关?

【问题讨论】:

    标签: java performance volatile


    【解决方案1】:

    根据您指出的文章,原因如下:

    易失性写入比非易失性写入要昂贵得多,因为需要内存隔离来保证可见性,但通常仍比锁定获取便宜。

    [...] 易失性读取很便宜——几乎与非易失性读取一样便宜

    当然,这是真的:内存栅栏操作总是绑定到写入,并且无论底层变量是否为易失性,读取都以相同的方式执行。

    但是,Java 中的volatile 不仅仅是易失性与非易失性内存读取。事实上,本质上它与这种区别无关:区别在于并发语义

    考虑这个臭名昭著的例子:

    volatile boolean runningFlag = true;
    
    void run() {
      while (runningFlag) { do work; }
    }
    

    如果 runningFlag 不是 volatile,则 JIT 编译器基本上可以将该代码重写为

    void run() {
       if (runningFlag) while (true) { do work; }
    }
    

    不用说,在每次迭代中读取runningFlag 与根本不读取它所引入的开销之比是巨大的。

    【讨论】:

    • 这么松散(考虑一下 Peter 和 DjDexter5GHz 提到的分布式缓存):
    • 如此松散(考虑到@Peter 和@DjDexter5GHz 提到的有关分布式缓存的位):runningFlag 在主内存中的堆上;更改为runningFlag 意味着写入主内存,然后根据需要写入二级和三级内存,然后再次从(最多)三级或主内存加载runningFlag?是否会为每次写入不同的内存位置构建一个内存栅栏,或者它是主内存中的一个内存栅栏,runningFlag 的所有值的更新都在其下进行协调?
    • 即使runningFlag 在主内存中保持不变,每次对runningFlag 的易失性读取都意味着至少一次或多次写入第三/第二内存存储?因此,volatile 的成本似乎是基于这样一个事实,即需要更多指令才能准确保证,但底层的内存和处理器架构——特别是现代多核处理器架构也对 volatile 的成本做出了不小的贡献?
    • 你的理解还是不够。例如,在 Intel CPU 架构上,根本不需要内存栅栏,因为 CPU 本身保证了正确的可见性和写入顺序。所以它只是一条mov 指令,而在不同的架构上,它最多需要一条额外的内存围栏指令。
    • 我不遵循您所说的“易失性读取意味着至少一次写入”的推理。不暗示写入,只是简单的 RAM 读取(同样是 x86 上的单个 mov 指令)。
    【解决方案2】:

    这是关于缓存的。由于新处理器使用缓存,如果您不指定易失性数据将保留在缓存中,并且写入操作很快。 (因为缓存靠近处理器)如果变量被标记为易失性,系统需要将它完全写入内存,这有点慢操作。

    是的,您的想法是正确的,它必须使用不同的线程堆栈做一些事情,因为每个线程堆栈都是独立的并且从相同的内存读取,但不一定从相同的缓存中读取。今天的处理器使用许多级别的缓存,因此如果多个线程/进程使用相同的数据,这可能是一个大问题。

    编辑:如果数据保留在本地缓存中,其他线程/进程将不会看到更改,直到数据被写回内存中。

    【讨论】:

      【解决方案3】:

      这很可能与易失性写入必须停止管道这一事实有关。

      所有写入都排队等待写入缓存。您不会在非易失性写入/读取中看到这一点,因为代码可以在不涉及缓存的情况下获取您刚刚写入的值。

      当您使用易失性读取时,它必须回到缓存中,这意味着在写入已写入的情况下写入(已实现)无法继续(如果您先写入然后读取)

      解决这个问题的一种方法是使用惰性写入,例如AtomicInteger.lazySet() 可以比 volatile 写入快 10 倍,因为它不等待。

      【讨论】:

      • 据我了解,lazySet实际上完全满足volatile的要求。区别仅在于写入可见性的及时性,对于volatile未指定。
      • AFAIK 写入同时对其他线程可见。 lazySet 的不同之处在于它可能对同一个线程不可见。
      • 是的,这就是volatile 的有趣之处:每个人假设写入将立即可见(或同步,以更详细地说明这个概念实质),并且它确实在所有已知的实现中立即可见;但这不是volatile 包含的语义。
      • 是的,lazySet 为您提供完全相同的东西,只是延迟可能更长。因此lazySet 的语义完全符合易失性写入 IMO 的语义。这就是lazySet 的奇怪之处:它暗示volatile 的意义远不止于此,但实际上并非如此。
      • 我说的是volatilelazySet的指定语义,所以参数是通用的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-04
      • 1970-01-01
      • 2013-05-23
      • 1970-01-01
      • 2018-01-18
      相关资源
      最近更新 更多