Java NIO - 内存映射文件

时间:2014-03-03 17:33:45

标签: java buffer nio channel memory-mapped-files

我最近遇到了这个article,它为内存映射文件提供了一个很好的介绍,以及它如何在两个进程之间共享。以下是读取文件的进程的代码:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MemoryMapReader {

 /**
  * @param args
  * @throws IOException 
  * @throws FileNotFoundException 
  * @throws InterruptedException 
  */
 public static void main(String[] args) throws FileNotFoundException, IOException, InterruptedException {

  FileChannel fc = new RandomAccessFile(new File("c:/tmp/mapped.txt"), "rw").getChannel();

  long bufferSize=8*1000;
  MappedByteBuffer mem = fc.map(FileChannel.MapMode.READ_ONLY, 0, bufferSize);
  long oldSize=fc.size();

  long currentPos = 0;
  long xx=currentPos;

  long startTime = System.currentTimeMillis();
  long lastValue=-1;
  for(;;)
  {

   while(mem.hasRemaining())
   {
    lastValue=mem.getLong();
    currentPos +=8;
   }
   if(currentPos < oldSize)
   {

    xx = xx + mem.position();
    mem = fc.map(FileChannel.MapMode.READ_ONLY,xx, bufferSize);
    continue;   
   }
   else
   {
     long end = System.currentTimeMillis();
     long tot = end-startTime;
     System.out.println(String.format("Last Value Read %s , Time(ms) %s ",lastValue, tot));
     System.out.println("Waiting for message");
     while(true)
     {
      long newSize=fc.size();
      if(newSize>oldSize)
      {
       oldSize = newSize;
       xx = xx + mem.position();
       mem = fc.map(FileChannel.MapMode.READ_ONLY,xx , oldSize-xx);
       System.out.println("Got some data");
       break;
      }
     }   
   }

  }

 }

}

然而,我对这种方法有一些意见/问题:

如果我们只在空文件上执行阅读器,即运行

  long bufferSize=8*1000;
  MappedByteBuffer mem = fc.map(FileChannel.MapMode.READ_ONLY, 0, bufferSize);
  long oldSize=fc.size();

这将分配8000个字节,现在将扩展该文件。返回的缓冲区的限制为8000,位置为0,因此,读者可以继续读取空数据。发生这种情况后,读者将停止,currentPos == oldSize

据说现在作者进来了(代码被省略了,因为大部分都是直截了当的,可以从网站上引用) - 它使用相同的缓冲区大小,因此它将先写入8000个字节,然后分配另外8000个,扩展文件。现在,如果我们假设这个过程暂停,然后我们回到阅读器,那么读者会看到文件的新大小并分配剩余部分(从位置8000到1600)并再次开始阅读,阅读另一个垃圾...

我有点困惑是否有同步这两个操作的原因。对我来说,任何对map的调用都可能会扩展文件,实际上是一个空缓冲区(用零填充),或者编写器可能只是扩展了文件,但还没有写入任何内容......

3 个答案:

答案 0 :(得分:12)

我为内存映射文件做了很多工作,用于进程间通信。我会 推荐Holger的#1或#2,但他的#3就是我的工作。但关键的一点是,我可能只与一位作家合作 - 如果你有多位作家,事情会变得更复杂。

文件的开头是一个标题部分,包含您需要的任何标题变量,最重要的是指向写入数据末尾的指针。在编写一段数据之后,编写者应该始终更新此头文件变量,读者不应该读取此变量。一个名为&#34;缓存一致性&#34;所有主流CPU都会保证读者会按照它们写入的相同顺序看到内存写入,因此如果您遵循这些规则,读者将永远不会读取未初始化的内存。 (例外情况是读者和作者在不同的服务器上 - 缓存一致性在那里不起作用。不要尝试在不同的服务器上实现共享内存!)

更新文件结束指针的频率没有限制 - 它全部在内存中并且不会涉及任何i / o,因此您可以更新每条记录或者你写的每条消息。

ByteBuffer拥有&#39; getInt()&#39;的版本和&#39; putInt()&#39;采用绝对字节偏移的方法,以便我用于读取和读取的内容。编写文件结束标记...在处理内存映射文件时,我从不使用相对版本。

你不应该使用文件大小或另一种进程间方法来传递文件结束标记,并且当你已经有共享内存时没有必要或好处。

答案 1 :(得分:5)

查看我的库Mappedbus(http://github.com/caplogic/mappedbus),它允许多个Java进程(JVM)为同一个内存映射文件写入记录。

以下是Mappedbus如何解决多个编写器之间的同步问题:

  • 文件的前八个字节组成一个名为limit的字段。此字段指定实际已将多少数据写入文件。读者将轮询限制字段(使用volatile)以查看是否有新记录要读取。

  • 当作者想要向文件添加记录时,它将使用fetch-and-add指令以原子方式更新限制字段。

  • 当限制字段增加时,读者将知道要读取的新数据,但更新限制字段的编写者可能尚未在记录中写入任何数据。为了避免这个问题,每个记录都包含一个构成提交字段的初始字节。

  • 当一个编写器写完一条记录时,它会设置一个提交字段(使用volatile),一旦看到提交字段被设置,读者只会开始读取一条记录。

    < / LI>

(顺便说一句,该解决方案仅经过验证,可以在Linux x86上使用Oracle的JVM。很可能无法在所有平台上运行。)

答案 2 :(得分:3)

有几种方法。

  1. 让作者在尚未编写的区域获得独占Lock。在写完所有内容后释放锁定。这与在该系统上运行的每个其他应用程序兼容,但它要求读者足够聪明以重试失败的读取,除非您将其与其他方法之一结合使用

  2. 使用其他通讯渠道,例如管道或套接字或文件的元数据通道,让作者告诉读者完成的写入。

  3. 在文件中的一个位置写一个特殊的标记(作为协议的一部分),告诉写入的数据,例如

    MappedByteBuffer bb;
    …
    // write your data
    
    bb.force();// ensure completion of all writes
    bb.put(specialPosition, specialMarkerValue);
    bb.force();// ensure visibility of the marker