multithreading可以在Java中看到直接映射的ByteBuffer上的写入吗?

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

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

鉴于这种情况:

  1. manager有一个映射的字节缓冲区B_all用于整个文件(比如它<2gb)
  2. 管理器调用B_all上的duplicate(),position(),limit()和slice()来创建一个新的较小的ByteBuffer B_1 ,该文件的一大块并将其提供给线程T1
  3. manager执行所有相同的操作来创建指向相同映射字节的ByteBuffer B_2并将其提供给线程T2

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

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

注意:我不是在寻找猜测(我可以自己做得很好)。 我想引用一些关于内存映射直接缓冲区保证(或不保证)的内容。 或者,如果您有实际经验或负面测试用例,那么这也可以作为充分的证据。

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

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

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

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

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

有关更多信息,请在posix中查找mmap(或者在Windows中使用CreateFileMapping,它的构建方式几乎相同。

不可以.JVM内存模型(JMM)不保证多个线程变异(不同步)数据会看到彼此的变化。

首先,鉴于访问共享内存的所有线程都在同一个JVM中,通过映射的ByteBuffer访问此内存的事实是无关紧要的(通过ByteBuffer访问的内存上没有隐式的volatile或同步),所以问题相当于一个关于访问字节数组的问题。

让我们重新解释这个问题,使其关于字节数组:

  1. 管理器有一个字节数组: byte[] B_all
  2. 创建对该数组的新引用: byte[] B_1 = B_all ,并给予线程T1
  3. 创建对该数组的另一个引用: byte[] B_2 = B_all ,并给予线程T2

通过线程T1写入B_1是否可以通过线程T2B_2看到?

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

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

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

我的部分答案是声称a ByteBuffer在数据同步方面与byte[]没有区别。 我找不到具体说明这一点的文档,但我建议java.nio.Buffer doc的“Thread Safety”部分提到有关同步或volatile的内容,如果适用的话。 由于文档没有提到这一点,我们不应该期望这样的行为。

您可以做的最便宜的事情是使用volatile变量。 在线程写入映射区域后,它应该将值写入volatile变量。 任何读取线程都应在读取映射缓冲区之前读取volatile变量。 这样做会在Java内存模型中产生“先发生过”。

请注意,您无法保证其他进程正在编写新内容。 但是如果你想保证其他线程可以看到你写的东西,写一个volatile(然后从读取线程中读取它)就可以了。

我认为直接内存提供与堆内存相同的保证或缺少它们。 如果修改共享底层数组或直接内存地址的ByteBuffer,则第二个ByteBuffer是另一个线程可以看到更改,但不保证这样做。

我怀疑即使你使用synchronized或volatile,它仍然不能保证工作,但它可能会这样做取决于平台。

在线程之间更改数据的简单方法是使用Exchanger

基于这个例子,

 class FillAndEmpty { final Exchanger exchanger = new Exchanger(); ByteBuffer initialEmptyBuffer = ... a made-up type ByteBuffer initialFullBuffer = ... class FillingLoop implements Runnable { public void run() { ByteBuffer currentBuffer = initialEmptyBuffer; try { while (currentBuffer != null) { addToBuffer(currentBuffer); if (currentBuffer.remaining() == 0) currentBuffer = exchanger.exchange(currentBuffer); } } catch (InterruptedException ex) { ... handle ... } } } class EmptyingLoop implements Runnable { public void run() { ByteBuffer currentBuffer = initialFullBuffer; try { while (currentBuffer != null) { takeFromBuffer(currentBuffer); if (currentBuffer.remaining() == 0) currentBuffer = exchanger.exchange(currentBuffer); } } catch (InterruptedException ex) { ... handle ...} } } void start() { new Thread(new FillingLoop()).start(); new Thread(new EmptyingLoop()).start(); } } 

我遇到的一个可能的答案是使用文件锁来获得对缓冲区映射的磁盘部分的独占访问权限。 例如, 这里用一个例子来解释。

我猜这将真正保护磁盘部分,以防止在同一文件部分进行并发写入。 对于磁盘文件的各个部分,使用基于Java的监视器可以实现相同的function(在单个JVM中,但对其他进程不可见)。 我猜测外部流程看不到的缺点会更快。

当然,如果jvm / os保证一致性,我想避免文件锁定或页面同步。

我不认为这是有保障的。 如果Java内存模型没有说它是保证的,那么根据定义它是不能保证的。 我会使用同步或队列写保护缓冲区写入来处理所有写入的一个线程。 后者与多核缓存很好地配合(最好每个RAM位置有1个写入器)。

不,它与普通的java变量或数组元素没有什么不同。