直接 ByteBuffer 之前发生

Happens-before for direct ByteBuffer

我在一个线程中有一个直接的 ByteBuffer(堆外),并使用 JMM 给我的一种机制将它安全地发布到另一个线程。 happens-before 关系是否扩展到由 ByteBuffer 包装的本机(堆外)内存?如果不是,我如何安全地将直接 ByteBuffer 的内容从一个线程发布到另一个线程?

编辑

这不是 Can multiple threads see writes on a direct mapped ByteBuffer in Java? 的副本,因为

编辑 2

这不是 Options to make Java's ByteBuffer thread safe 的副本我不是要同时修改来自两个不同线程的 ByteBuffer。我试图将 if 从一个线程移交给另一个线程,并在由直接 ByteBuffer 支持的本机内存区域上获得 happens-before 语义。第一个线程在移交后将不再修改或读取 ByteBuffer。

Java 对象监视器的 happens-before 顺序语义在 §17.4.5 中描述为:

The wait methods of class Object (§17.2.1) have lock and unlock actions associated with them; their happens-before relationships are defined by these associated actions.

未指定这仅适用于 Java 管理的对象还是适用于任何数据。毕竟,Java 并不关心 Java "world" 之外发生的事情。但这也意味着我们可以将规范外推到 Java 世界中的任何数据 reachable。那么与堆的关系就变得不那么重要了。毕竟,如果我同步线程,为什么它不能直接用于 ByteBuffer?

为了确认这一点,我们可以看一下它在 OpenJDK 中的实际实现方式。

如果我们仔细观察,我们会发现 ObjectMonitor::wait,除此之外 does:

    OrderAccess::fence();

ObjectMonitor::exitnotify/notifyAll的业务结束)does:

    OrderAccess::release_store_ptr (&_owner, NULL) ;
    OrderAccess::storeload() ;

fence()storeload() 都会导致全局 StoreLoad 内存栅栏:

inline void OrderAccess::storeload()  { fence(); }

在 SPARC 上它生成 membar 指令:

  __asm__ volatile ("membar  #StoreLoad" : : :);

在 x86 上它是 to membar(Assembler::StoreLoad) and subsequently:

  // Serializes memory and blows flags
  void membar(Membar_mask_bits order_constraint) {
    if (os::is_MP()) {
      // We only have to handle StoreLoad
      if (order_constraint & StoreLoad) {
        // All usable chips support "locked" instructions which suffice
        // as barriers, and are much faster than the alternative of
        // using cpuid instruction. We use here a locked add [esp],0.
        // This is conveniently otherwise a no-op except for blowing
        // flags.
        // Any change to this code may need to revisit other places in
        // the code where this idiom is used, in particular the
        // orderAccess code.
        lock();
        addl(Address(rsp, 0), 0);// Assert the lock# signal here
      }
    }
  }

就是这样,它只是 CPU 级别的内存障碍。引用计数和垃圾收集在更高级别发挥作用。

这意味着至少在 OpenJDK 中,在 Object.notify 之前发出的 任何 内存写入将在 发出的任何读取之前排序在 Object.wait.

之后

当然,如果您在 Java 代码中读写 ByteBuffer,使用 Java 方法,例如 putget,那么会发生 -在第一个线程上的修改 publishing/consumption 与第二个线程上的最终后续访问之间的关系之前,将以预期的方式应用 0。毕竟 ByteBuffer 由 "off heap" 内存支持这一事实只是一个实现细节:它不允许 ByteBuffer 上的 Java 方法破坏内存模型契约.

如果您谈论从通过 JNI 或其他机制调用的 本机代码 写入此字节缓冲区,事情就会变得有点模糊。我认为只要您在本机代码中使用普通存储(即,不是非临时存储或任何语义比普通存储弱的东西),您在实践中就会很好。毕竟 JMV 在内部通过相同的机制实现了对堆内存的存储,特别是 getput 类型的方法将通过正常的加载和存储来实现。发布操作通常涉及某种类型的发布存储,将应用于所有先前的 Java 操作以及您本机代码中的存储。

您可以在或多或少涉及此主题的并发邮件列表中找到一些 expert discussion。确切的问题是 "Can I use Java locks to protect a buffer accessed only by native code",但潜在的问题几乎相同。结论似乎与上面一致:如果你正常加载和存储到一个正常的1内存区域是安全的。如果你想使用较弱的指令,你需要一个栅栏。


0 所以这是一个有点冗长、折磨人的句子,但我想说清楚的是,有一整条 happens-before 对必须是正确同步以使其工作:(A) 在写入缓冲区和第一个线程上的发布存储之间,(B) 发布存储和消耗负载 (C) 消耗负载和第二个线程的后续读取或写入线。对 (B) 完全在 Java-land 中,因此遵循常规规则。那么问题主要是关于具有一个 "native" 元素的 (A) 和 (C) 是否也可以。

1 Normal 在这种情况下或多或少意味着 Java 使用的相同类型的内存区域,或者在至少一个在内存类型 Java 使用方面具有很强的一致性保证。你必须竭尽全力违反这一点,因为你正在使用 ByteBuffer 你已经知道该区域是由 Java 分配的并且必须按照正常规则进行游戏(因为 [=47= ByteBuffer 上的 ] 级方法至少需要以与内存模型一致的方式工作。