25

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

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

鉴于这种情况:

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

我的问题是:T1 写 B_1 和 T2 写 B_2 可以同时保证看到对方的变化吗?T3 是否可以使用 B_all 读取这些字节并保证看到 T1 和 T2 的更改?

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

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

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

4

7 回答 7

16

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

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

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

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

有关更多信息,请查看 posix 中的 mmap(或 Windows 的 CreateFileMapping,其构建方式几乎相同。

于 2011-08-11T04:11:50.910 回答
5

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

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

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

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

线程写入是否会被B_1线程T1看到?B_2T2

T_1不,如果和之间没有一些明确的同步,则不能保证看到这样的写入T_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在数据同步方面与aByteBuffer没有什么不同。byte[]我找不到说明这一点的具体文档,但我建议java.nio.Buffer文档的“线程安全”部分会提到有关同步或易失性的内容(如果适用的话)。由于文档没有提到这一点,我们不应该期望这种行为。

于 2012-04-19T07:51:12.560 回答
3

The cheapest thing you can do is use a volatile variable. After a thread writes to the mapped area, it should write a value to a volatile variable. Any reading thread should read the volatile variable before reading the mapped buffer. Doing this produces a "happens-before" in the Java memory model.

Note that you have NO guarantee that another process is in the middle of writing something new. But if you want to guarantee that other threads can see something you've written, writing a volatile (followed by reading it from the reading thread) will do the trick.

于 2012-02-17T05:32:16.473 回答
1

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

我怀疑即使您使用同步或易失性,它仍然不能保证工作,但是根据平台它可能会这样做。

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

基于示例,

class FillAndEmpty {
   final Exchanger<ByteBuffer> exchanger = new Exchanger<ByteBuffer>();
   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();
   }
 }
于 2011-08-09T20:37:45.880 回答
1

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

我猜这会真正保护磁盘部分,以防止对文件的同一部分进行并发写入。使用基于 Java 的磁盘文件部分监视器可以实现同样的事情(在单个 JVM 中,但对其他进程不可见)。我猜这会更快,因为它对外部进程不可见。

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

于 2011-08-10T15:03:25.157 回答
0

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

于 2011-08-09T20:36:43.217 回答
0

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

于 2011-08-09T21:53:58.923 回答