【问题标题】:Emulating a memory barrier in Java to get rid of volatile reads在 Java 中模拟内存屏障以消除易失性读取
【发布时间】:2020-02-08 17:07:29
【问题描述】:

假设我有一个同时访问的字段,它被多次读取,很少写入。

public Object myRef = new Object();

假设一个线程 T1 将 myRef 设置为另一个值,每分钟一次,而其他 N 个线程将连续并发地读取 myRef 数十亿次。我只需要 myRef 最终对所有线程可见。

一个简单的解决方案是使用 AtomicReference 或像这样简单地使用 volatile:

public volatile Object myRef = new Object();

但是,afaik 易失性读取确实会产生性能成本。我知道这是微不足道的,这更像是我想知道的东西,而不是我真正需要的东西。所以我们不要关心性能,假设这是一个纯粹的理论问题。

所以问题归结为:有没有办法通过在写入站点做一些事情来安全地绕过对很少写入的引用的易失性读取?

经过一番阅读,看起来记忆障碍可能是我所需要的。因此,如果存在这样的构造,我的问题将得到解决:

  • 调用屏障(同步)
  • 一切都已同步,所有线程都将看到新值。 (在读取站点没有永久成本,它可能是陈旧的或在缓存同步时产生一次性成本,但在此之后它会全部返回到常规字段直到下一次写入)。

在 Java 中或一般情况下是否有这样的构造?在这一点上,我不禁想到,如果存在这样的东西,那么维护这些的更聪明的人已经将它合并到原子包中。 (不成比例的频繁读写可能不是一个需要关心的情况?)所以也许我的想法有问题,这样的构造根本不可能?

我已经看到一些代码示例出于类似目的使用“volatile”,利用它的发生前合同。有一个单独的同步字段,例如:

public Object myRef = new Object();
public volatile int sync = 0;

在写线程/站点时:

myRef = new Object();
sync += 1 //volatile write to emulate barrier

我不确定这是否有效,有些人认为这仅适用于 x86 架构。在阅读了 JMS 中的相关部分之后,我认为只有在 volatile 写入与来自需要查看 myRef 新值的线程的 volatile 读取相结合时,才能保证工作。 (所以不会摆脱 volatile 读取)。

回到我原来的问题;这可能吗?在Java中可能吗?是否可以在 Java 9 VarHandles 中的新 API 之一中使用?

【问题讨论】:

  • 对我来说,听起来您已经进入了需要编写和运行一些模拟工作负载的实际基准测试的领域。
  • JMM 声明如果您的写入线程执行sync += 1; 并且您的读取线程读取了sync 值,他们也会看到myRef 更新。因为您只需要读者最终看到更新,因此您可以利用这一优势,仅在阅读器线程的每 1000 次迭代或类似情况下读取同步。但是您也可以使用 volatile 做类似的技巧 - 只需在读取器中缓存 myRef 字段进行 1000 次迭代,然后使用 volatile 再次读取它...
  • @PetrJaneček 但是他不需要同步对线程之间共享的计数器变量的访问吗?不会是瓶颈吗?在我看来,这将更加昂贵。
  • @RavindraRanwala 每个读者都有自己的计数器,如果你的意思是数到 1000 次左右的迭代。如果您的意思是sync 字段,不,读者不会在每次迭代时触摸sync 字段,当他们想检查是否有更新时,他们会机会主义地这样做。也就是说,一个更简单的解决方案是将 myRef 缓存 1000 轮,然后重新读取它...
  • @PetrJaneček 谢谢,我认为这是一个可能的解决方案。但我想知道这是否可以使用通用、可靠的实现。

标签: java memory concurrency volatile


【解决方案1】:

所以基本上你想要 volatile 的语义而不需要运行时成本。

我认为这是不可能的。

问题在于volatile 的运行时成本是由于在写入器和读取器代码中实现内存屏障的指令造成的。如果您通过摆脱其内存屏障来“优化”阅读器,那么您将不再保证阅读器在实际写入时会看到“很少写入”的新值。

FWIW,sun.misc.Unsafe 类的某些版本提供了显式的loadFencestoreFencefullFence 方法,但我认为使用它们不会比使用volatile 带来任何性能优势。


假设 ...

您希望多处理器系统中的一个处理器能够告诉所有其他处理器:

“嘿!无论你在做什么,都要使地址 XYZ 的内存缓存无效,然后立即执行。”

很遗憾,现代 ISA 不支持这一点。

实际上,每个处理器都控制自己的缓存。

【讨论】:

  • 我明白了,假设您的答案中的部分是我所追求的。谢谢。
【解决方案2】:

不太确定这是否正确,但我可能会使用队列来解决这个问题。

创建一个包装 ArrayBlockingQueue 属性的类。该类有一个更新方法和一个读取方法。 update 方法将新值发布到队列中,并删除除最后一个值之外的所有值。 read 方法返回对队列进行 peek 操作的结果,即读取但不删除。窥视队列前面元素的线程不受阻碍。更新队列的线程干净利落。

【讨论】:

    【解决方案3】:
    • 您可以使用ReentrantReadWriteLock,它专为少写多读的场景而设计。
    • 您可以使用StampedLock,它专为少写多读的相同情况而设计,但也可以乐观地尝试读取。示例:

      private StampedLock lock = new StampedLock();
      
      public void modify() {            // write method
          long stamp = lock.writeLock();
          try {
            modifyStateHere();
          } finally {
            lock.unlockWrite(stamp);
          }
      } 
      
      public Object read() {            // read method
        long stamp = lock.tryOptimisticRead();
        Object result = doRead();       //try without lock, method should be fast
        if (!lock.validate(stamp)) {    //optimistic read failed
          stamp = lock.readLock();      //acquire read lock and repeat read
          try {
            result = doRead();
          } finally {
            lock.unlockRead(stamp);
          }
        }
        return result;
      }
      
    • 使您的状态为 immutable 并仅通过克隆现有对象并通过构造函数仅更改必要的属性来允许受控修改。一旦构造了新状态,就将其分配给许多读取线程正在读取的引用。这样读取线程会产生零成本

    【讨论】:

    • 如果您想投反对票,请说明原因,以便作者和社区学习
    • 在我的场景中不可能让它不可变。如果标记锁盒的成本低于简单的易失性读取,我会感到非常惊讶。不过我会试试的,谢谢。
    【解决方案4】:

    X86 提供 TSO;您可以免费获得 [LoadLoad][LoadStore][StoreStore] 栅栏。

    易失性读取需要释放语义。

    r1=Y
    [LoadLoad]
    [LoadStore]
    ...
    

    如您所见,X86 已经免费提供了这个功能。

    在您的情况下,大多数调用都是读取,并且缓存行已经在本地缓存中。

    编译器级别的优化需要付出代价,但在硬件级别上,易失性读取与常规读取一样昂贵。

    另一方面,volatile 写入更昂贵,因为它需要 [StoreLoad] 来保证顺序一致性(在 JVM 中,这是使用 lock addl %(rsp),0 或 MFENCE 完成的)。由于在您的情况下很少写入,因此这不是问题。

    我会小心优化这一级别,因为很容易使代码比实际需要的更复杂。最好通过一些基准来指导您的开发工作,例如使用 JMH,最好在真实硬件上进行测试。也可能隐藏着其他令人讨厌的生物,例如虚假分享。

    【讨论】:

      猜你喜欢
      • 2017-01-17
      • 2019-11-09
      • 2010-12-19
      • 2018-02-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-28
      • 2018-01-18
      相关资源
      最近更新 更多