11

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

编辑

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

  • 我说的不是 mmaped() 区域,而是一般的堆外区域
  • 我正在安全地发布 ByteBuffer
  • 我没有同时修改 ByteBuffer 的内容,我只是将它从一个线程转移到另一个线程

编辑 2

这不是使 Java 的 ByteBuffer 线程安全的选项的副本我不想同时从两个不同的线程修改 ByteBuffer。我正在尝试将是否从一个线程移交给另一个线程,并在由直接 ByteBuffer 支持的本机内存区域上获得发生前的语义。一旦移交,第一个线程将不再修改或读取 ByteBuffer。

4

2 回答 2

3

当然,如果您使用和ByteBuffer等 Java 方法读写Java 代码,那么您在第一个线程上的修改、发布/消费以及最后在第二个线程上的后续访问之间的发生前关系将在预期中应用0方式。毕竟由“堆外”内存支持的事实只是一个实现细节:它不允许Java方法破坏内存模型契约。putgetByteBufferByteBuffer

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

您可以在或多或少的这个主题的并发邮件列表上找到一些专家讨论。确切的问题是“我可以使用 Java 锁来保护只能由本机代码访问的缓冲区”,但基本问题几乎相同。结论似乎与上述一致:如果您进行正常加载并存储到正常的1内存区域是安全的。如果你想使用较弱的指令,你需要一个栅栏。


0所以这句话有点冗长、折磨人,但我想澄清的是,有一整串发生前发生的对必须正确同步才能工作:(A) 在写入到缓冲区和第一个线程上的发布存储,(B)发布存储和消费负载(C)消费负载以及第二个线程的后续读取或写入。对 (B) 纯粹是在 Java 领域,因此遵循常规规则。那么问题主要是关于具有一个“原生”元素的(A)和(C)是否也可以。

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

于 2017-11-01T21:34:35.860 回答
2

Java 对象监视器的先发生顺序语义在§17.4.5中描述为:

wait类(第 17.2.1 节)的方法Object具有与之关联的锁定和解锁操作;它们的happens-before关系由这些关联的动作定义。

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

为了确认这一点,我们可以看看它是如何在 OpenJDK 中实际实现的。

如果我们仔细观察,我们会发现ObjectMonitor::wait除其他外:

    OrderAccess::fence();

并且( /ObjectMonitor::exit的业务端)确实notifynotifyAll

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

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

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

在 SPARC 上,它生成membar指令:

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

在 x86 上,它转到 membar(Assembler::StoreLoad)随后

  // 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

于 2017-11-01T21:02:42.637 回答