Java NIO - 内存映射文件
Posted
技术标签:
【中文标题】Java NIO - 内存映射文件【英文标题】:Java NIO - Memory mapped files 【发布时间】:2014-04-04 21:23:51 【问题描述】:我最近遇到了这个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
的任何调用都可能使用真正的空缓冲区(用零填充)来扩展文件,或者作者可能刚刚扩展了文件,但还没有写入任何内容......
【问题讨论】:
每当我看到“写入”和“共享数据”时,我认为需要同步。 我不知道你所说的“是否有同步的原因”是什么意思,但是打开大量内存映射文件,或者多次打开同一个文件,无论如何都是一个非常糟糕的主意,因为垃圾收集的原因,因为没有明确定义的时间可以释放相关内存。并且像 8k 这样的少量映射并没有特别的优势:您也可以只使用缓冲流,默认情况下具有那么多缓冲,并且没有关于文件扩展时要做什么的问题。映射文件最好用于极少数(例如一个)非常大的文件。 好的,知道了 - 打开一个大文件。尽管如此,这仍然是 IPC 的平均值,所以我想知道如何实现这一点,即一个进程写入,另一个进程读取,但在某种程度上,我们知道另一个进程在我们读取它之前实际上已经写了一些东西.这就是我说的同步 这是一个文件而不是管道。单独使用 mmap() 将不允许您进行同步。示例代码会(丑陋的)忙轮询。 【参考方案1】:查看我的库 Mappedbus (http://github.com/caplogic/mappedbus),它允许多个 Java 进程 (JVM) 将记录写入同一个内存映射文件。
以下是 Mappedbus 如何解决多个写入器之间的同步问题:
文件的前 8 个字节构成一个称为限制的字段。此字段指定实际写入文件的数据量。读取器将轮询限制字段(使用 volatile)以查看是否有新记录要读取。
当编写者想要向文件中添加记录时,它将使用 fetch-and-add 指令以原子方式更新限制字段。
当限制字段增加时,读取器将知道有新数据要读取,但更新限制字段的写入器可能尚未在记录中写入任何数据。为了避免这个问题,每条记录都包含一个组成提交字段的初始字节。
当写入器完成写入记录时,它将设置提交字段(使用 volatile),读取器只有在看到提交字段已设置后才会开始读取记录。
(顺便说一句,该解决方案仅经验证可在带有 Oracle JVM 的 Linux x86 上运行。它很可能无法在所有平台上运行)。
【讨论】:
是的,您使用 CAS 的解决方案更好。你用过 Unsafe.compareAndSwapInt(...) 吗? 是的,确切地说是 compareAndSwapLong。看一下MappedBusWriter.java中的allocate方法 最初该解决方案使用 CAS,但现在使用 fetch-and-add 作为优化【参考方案2】:我在内存映射文件方面做了很多工作以进行进程间通信。我会不推荐 Holger 的 #1 或 #2,但他的 #3 是我所做的。但关键点可能是我只与一个作家合作 - 如果你有多个作家,事情会变得更加复杂。
文件的开头是一个标题部分,其中包含您需要的任何标题变量,最重要的是指向写入数据末尾的指针。写入者应该总是在写入一段数据之后更新这个头变量,而读取者永远不应该读取超出这个变量的内容。所有主流 CPU 都拥有的一种称为“缓存一致性”的东西将保证读取器将按照写入顺序看到内存写入,因此如果您遵循这些规则,读取器将永远不会读取未初始化的内存。 (一个例外是读写器在不同的服务器上——缓存一致性在那里不起作用。不要试图在不同的服务器上实现共享内存!)
对文件结尾指针的更新频率没有限制——它都在内存中,不会涉及任何 i/o,因此您可以在每条记录或您编写的每条消息中更新它。
ByteBuffer 有 'getInt()' 和 'putInt()' 方法的版本,它们采用绝对字节偏移量,所以这就是我用来读取和写入文件结尾标记的方法......我从不使用相对处理内存映射文件时的版本。
如果您已经拥有共享内存,则不应该使用文件大小或其他进程间方法来传达文件结束标记,并且没有必要或受益。
【讨论】:
+1。您可以说明如何防止两个编写者同时尝试在同一位置扩展文件。我还看到了用于自己实现锁的标头。 存储顺序将在 x86 上得到尊重,但不会在任何其他“主流”CPU 上得到尊重。此外,您的编译器/JVM 可能被允许重新排序您的商店(如果它们不是易失性或有序的。) 所有主流编译器都会遵守存储顺序。它被称为“缓存一致性”。开发了一些不尊重“缓存一致性”的实验性 CPU,但它们从未成为主流。多年来,我在许多计算机、Windows 和 Unix 上都使用了这种技术。 请记住,我们不是将 POJO 存储在共享内存中,而是使用 'getInt()' 等方法,这里的所有内容都是隐式易失的。 这个问题特别提到了“java”和“interprocess”。因此,我们谈论的是 MappedByteBuffer 和 getInt()。为什么每个人都在谈论仅与 C++ 和/或线程相关的问题?【参考方案3】:有几种方法。
让写入者在尚未写入的区域上获取一个独占的Lock
。写完所有内容后释放锁。这与在该系统上运行的所有其他应用程序兼容,但它要求读取器足够聪明以重试失败的读取,除非您将其与其他方法之一结合使用
使用其他通信渠道,例如一个管道、一个套接字或一个文件的元数据通道,让作者告诉读者完成的写入。
在文件中的某个位置写入一个特殊标记(作为协议的一部分),说明写入的数据,例如
MappedByteBuffer bb;
…
// write your data
bb.force();// ensure completion of all writes
bb.put(specialPosition, specialMarkerValue);
bb.force();// ensure visibility of the marker
【讨论】:
锁定是指频道上的FileLock
?
是的,我的意思是FileLock
。以上是关于Java NIO - 内存映射文件的主要内容,如果未能解决你的问题,请参考以下文章
java大文件读写操作,java nio 之MappedByteBuffer,高效文件/内存映射
JavaNIO的深入研究4内存映射文件I/O,大文件读写操作,Java nio之MappedByteBuffer,高效文件/内存映射
作者推荐 | Java难点攻克「NIO和内存映射性能提升系列」彻底透析NIO底层的内存映射机制原理与Direct Memory的关系