多个线程能否在 Java 中直接映射的 ByteBuffer 上看到写入?

我正在研究使用从内存映射文件(通过FileChannel.map())构建的ByteBuffers以及内存中直接ByteBuffers的东西。我试图理解并发和内存模型约束。

我已经阅读了所有相关的Javadoc(和源代码),如FileChannel,ByteBuffer,MappedByteBuffer等。很明显,特定的ByteBuffer(和相关子类)有一堆字段,并且从内存模型的角度来看,状态不受保护。因此,如果在跨线程使用特定 ByteBuffer 的状态时,必须进行同步。常见的技巧包括使用ThreadLocal包装ByteBuffer,复制(同步时)以获得指向相同映射字节的新实例等。

给定此方案:

  1. 管理器具有整个文件的映射字节缓冲区(假设它是<2gb)B_all
  2. 管理器在B_all上调用replicate(),position(),limit()和slice()以创建一个新的较小的ByteBuffer,该文件的一个块,并将其提供给线程T1B_1
  3. 管理器执行所有相同的操作来创建指向相同映射字节的字节缓冲区,并将其提供给线程 T2B_2

我的问题是:T1可以同时写入B_1,T2写入B_2并保证看到彼此的变化吗?T3 能否使用B_all来读取这些字节,并保证同时看到 T1 和 T2 的变化?

我知道映射文件中的写入不一定在进程中看到,除非您使用force()指示操作系统将页面写入磁盘。我不在乎这些。对于这个问题,假设此 JVM 是编写单个映射文件的唯一进程。

注意:我不是在寻找猜测(我自己可以很好地做出这些猜测)。我希望引用一些关于内存映射直接缓冲区保证(或不保证)的明确信息。或者,如果您有实际经验或阴性测试用例,那也可以作为充分的证据。

更新:我已经做了一些测试,让多个线程并行写入同一文件,到目前为止,这些写入似乎立即从其他线程中可见。我不确定我是否可以依靠它。


答案 1

使用JVM的内存映射只是CreateFileMapping(Windows)或mmap(posix)的薄包装。因此,您可以直接访问操作系统的缓冲区缓存。这意味着这些缓冲区是操作系统认为文件包含的内容(操作系统最终将同步文件以反映这一点)。

因此,无需调用 force() 在进程之间进行同步。进程已经同步(通过操作系统 - 甚至读/写访问相同的页面)。强制操作系统和驱动器控制器之间仅同步(驱动器控制器和物理盘片之间可能会有一些延迟,但您没有硬件支持来执行任何操作)。

无论如何,内存映射文件是线程和/或进程之间共享内存的可接受形式。这种共享内存与Windows中的命名虚拟内存块之间的唯一区别是最终同步到磁盘(实际上mmap通过映射/dev/null来执行没有文件事物的虚拟内存)。

从多个进程/线程读取写入内存仍然需要一些同步,因为处理器能够执行无序执行(不确定这与JVM的交互程度,但您无法做出假设),但是从一个线程写入字节将具有与通常写入堆中的任何字节相同的保证。写入它后,每个线程和每个进程都将看到更新(即使通过打开/读取操作)。

有关详细信息,请在 posix 中查找 mmap(或 CreateFileMapping for Windows,其构建方式几乎相同。


答案 2

不。JVM 内存模型 (JMM) 不保证多个线程在突变(未同步)数据时会看到彼此的变化。

首先,假设所有访问共享内存的线程都位于同一个 JVM 中,因此通过映射的 ByteBuffer 访问此内存的事实是无关紧要的(通过 ByteBuffer 访问的内存上没有隐式易失性或同步),因此这个问题等效于访问字节数组的问题。

让我们重新表述这个问题,让它与字节数组有关:

  1. 管理器有一个字节数组:byte[] B_all
  2. 将创建对该数组的新引用:,并将其提供给 threadbyte[] B_1 = B_allT1
  3. 创建对该数组的另一个引用:,并提供给线程byte[] B_2 = B_allT2

按线程写入的操作是否在按线程中看到?B_1T1B_2T2

否,如果不在 和 之间进行一些显式同步,则不能保证看到此类写入操作。问题的核心是JVM的JIT,处理器和内存架构可以自由地重新排序一些内存访问(不仅仅是为了惹恼你,而且是通过缓存来提高性能)。所有这些层都希望软件能够明确(通过锁,易失性或其他显式提示)需要同步的位置,这意味着当没有提供此类提示时,这些层可以自由地移动东西。T_1T_2

请注意,在实践中,您是否看到写入操作主要取决于硬件以及不同级别的缓存和寄存器中数据的对齐方式,以及正在运行的线程在内存层次结构中的“距离”。

JSR-133 是一项精确定义 Java 内存模型的努力,大约是 Java 5.0(据我所知,它在 2012 年仍然适用)。这就是你想要寻找明确(尽管密集)答案的地方:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf(第2节最相关)。可以在JMM网页上找到更多可读性的东西:http://www.cs.umd.edu/~pugh/java/memoryModel/

我的部分答案是断言a在数据同步方面与a没有什么不同。我找不到具体的文档来说明这一点,但我建议java.nio.Buffer文档的“线程安全”部分会提到一些关于同步或易失性的东西,如果适用的话。由于文档没有提到这一点,我们不应该期望这样的行为。ByteBufferbyte[]


推荐