【问题标题】:How does the JSR-133 cookbook enforce all the guarantees made by the Java Memory ModelJSR-133 食谱如何执行 Java 内存模型做出的所有保证
【发布时间】:2016-11-19 18:27:16
【问题描述】:

我的理解是,JSR-133 cookbook 是关于如何使用一系列内存屏障(或至少是可见性保证)实现 Java 内存模型的一个被广泛引用的指南。

根据对不同类型屏障的描述,我的理解也是,StoreLoad 是唯一保证所有 CPU 缓冲区都刷新到缓存并因此确保新读取(通过避免存储转发)并保证观察由于缓存一致性,最新的值。

我正在查看易失性/常规存储/加载的不同程序顺序交错所需的特定障碍表以及需要哪些内存障碍。

根据我的直觉,这张表似乎不完整。例如,Java 内存模型保证监视器的获取操作对在另一个线程中发布之前执行的所有操作的可见性,即使正在更新的值是非易失性的。在上面链接的表格中,似乎刷新 CPU 缓冲区和传播更改/允许观察新更改的唯一操作是 Volatile Store 或 MonitorExit,然后是 Volatile Load 或 MonitorEnter。我看不到在我上面的示例中障碍如何保证可见性,当这些操作(根据表)仅使用 LoadStore 和 StoreStore 时,据我所知,它们只涉及线程中的重新排序并且不能强制执行之前发生的事情保证(即跨线程)。

我在这里的理解哪里出错了?或者这个实现是否只强制发生在之前而不是同步保证或获取/释放监视器的额外操作。

谢谢

【问题讨论】:

    标签: java multithreading concurrency cpu cpu-cache


    【解决方案1】:

    文档中的障碍是抽象概念,或多或少映射到不同 CPU 上的不同事物。但它们只是指导方针。 JVM 实际上必须遵循的规则是 JLS 第 17 章中的规则。

    障碍作为一个概念也是“全球性的”,因为它们在指令之前和之后排列所有

    例如,Java 内存模型保证监视器的获取操作对在另一个线程中发布之前执行的所有操作的可见性,即使正在更新的值是非易失性的。

    获取一个监视器是cookbook中的monitor-enter,它只需要对其他争夺锁的线程可见。 monitor-exit 是释放操作,它将防止加载和存储在它之前移动到它下面。您可以在食谱表中看到这一点,其中第一个操作是正常的加载/存储,第二个操作是 volatile-store 或 monitor-exit。

    在具有 Total Store Order 的 CPU 上,可用的存储缓冲区对正确性没有影响;只看性能。

    在任何情况下,由 JVM 来使用提供 JLS 要求的原子性和可见性语义的指令。这就是关键的要点:如果您编写 Java 代码,您将针对 JLS 中定义的抽象机器进行编码。如果仅对抽象机器进行编码不能为您提供所需的性能,您只会深入研究具体机器的实现细节。你不需要为了正确而去那里。

    【讨论】:

      【解决方案2】:

      StoreLoad 是唯一保证所有 CPU 缓冲区都被刷新到缓存的方法,因此可以确保新的读取(通过避免存储转发)并保证观察到由于缓存一致性而导致的最新值。

      这可能适用于 x86 架构,但您不应该考虑那种抽象级别。缓存一致性对于正在执行的处理器来说可能代价高昂。

      以移动设备为例,一个重要的目标是减少电池使用程序的消耗量。在这种情况下,它们可能不参与缓存一致性,StoreLoad 将失去此功能。

      在我上面的示例中,当这些操作(根据表格)仅使用 LoadStore 和 StoreStore 时,我看不到障碍如何保证可见性,据我了解,它们只涉及线程中的重新排序,不能强制执行发生在保证之前(即跨线程)。

      让我们只考虑一个 volatile 字段。不稳定的加载和存储看起来如何?好吧,Aleksey Shipilëv has a great write up on this,但我会分一杯羹。

      易失性存储,然后是后续加载:

      <other ops>
      [StoreStore]
      [LoadStore]
      x = 1; // volatile store
      [StoreLoad] // Case (a): Guard after volatile stores
      
      ...
      
      [StoreLoad] // Case (b): Guard before volatile loads
      int t = x; // volatile load
      [LoadLoad]
      [LoadStore]
      <other ops>
      

      因此,&lt;other ops&gt; 可以是非易失性写入,但正如您所见,这些写入在易失性存储之前提交到内存。那么当我们准备好读取LoadLoad时,LoadStore会强制等待,直到volatile store成功。

      最后,StoreLoad before 和 after 确保了 volatile 加载和存储如果紧接在另一个之前就不能重新排序。

      【讨论】:

        【解决方案3】:

        我不确定你从哪里得到StoreLoad 障碍是唯一强制执行某些特定行为的类型。抽象地说,所有的障碍都严格执行它们被定义为强制执行的内容。例如,LoadLoad 可防止任何先前加载与任何后续加载重新排序。

        可能有架构特定的描述来说明特定屏障是如何实施的:例如,在 x86 上,除了StoreLoad 之外的所有屏障都是无操作的,因为芯片架构自动执行其他排序,StoreLoad 通常实现为存储缓冲区刷新。尽管如此,所有障碍都有其与体系结构无关的抽象定义,并且说明书是根据它定义的,以及将概念障碍映射到实际 ISA 特定实现的映射。

        特别是,即使障碍在特定平台上是“无操作”,也意味着保留了顺序,因此满足了所有发生前发生和其他同步要求。

        【讨论】:

          猜你喜欢
          • 2022-08-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-07-03
          • 1970-01-01
          • 2016-09-22
          • 1970-01-01
          相关资源
          最近更新 更多