Bob*_*r02 6 java buffer nio channel memory-mapped-files
我最近遇到过这篇文章,它为内存映射文件以及如何在两个进程之间共享提供了一个很好的介绍.以下是读入文件的进程的代码:
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;
}
}
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
但是,我对这种方法提出了一些意见/问题:
如果我们仅在空文件上执行读取器,即运行
long bufferSize=8*1000;
MappedByteBuffer mem = fc.map(FileChannel.MapMode.READ_ONLY, 0, bufferSize);
long oldSize=fc.size();
Run Code Online (Sandbox Code Playgroud)
这将分配8000个字节,现在将扩展该文件.返回的缓冲区的限制为8000,位置为0,因此,读者可以继续读取空数据.在这发生之后,读者将停止,如currentPos == oldSize.
据说现在编写进来(代码被省略,因为大部分内容都很简单,可以从网站上引用) - 它使用相同的缓冲区大小,因此它将首先写入8000个字节,然后分配另外8000个,扩展文件.现在,如果我们假设这个过程暂停,然后我们回到阅读器,那么读者会看到文件的新大小并分配剩余部分(从位置8000到1600)并再次开始阅读,阅读另一个垃圾...
我是否有同步这两个操作的原因有点困惑.据我所知,任何调用都map可能使用一个空缓冲区(用零填充)扩展文件,或者编写器可能刚刚扩展了文件,但还没有写入任何内容...
Tim*_*per 12
我使用内存映射文件进行大量工作以进行进程间通信.我不会推荐Holger的#1或#2,但他的#3就是我做的.但关键的一点是,我可能只与一位作家合作 - 如果你有多个作家,事情会变得更复杂.
文件的开头是一个标题部分,包含您需要的任何标题变量,最重要的是指向写入数据末尾的指针.编写者应该在写完一段数据后更新这个标题变量,读者永远不应该超越这个变量.所有主流CPU都称之为"缓存一致性"的东西将保证读者会按照它们所写的相同顺序看到内存写入,因此如果遵循这些规则,读者将永远不会读取未初始化的内存.(例外情况是读者和作者在不同的服务器上 - 缓存一致性在那里不起作用.不要试图在不同的服务器上实现共享内存!)
更新文件结束指针的频率没有限制 - 它全部在内存中,并且不会涉及任何i/o,因此您可以更新每条记录或您编写的每条消息.
ByteBuffer有'getInt()'和'putInt()'方法的版本,它们采用绝对字节偏移量,这就是我用来读取和写入文件结束标记的...我工作时从不使用相对版本内存映射文件.
你不应该使用文件大小或另一个进程间方法来传递文件结束标记,并且当你已经有共享内存时没有必要或好处.
查看我的库Mappedbus(http://github.com/caplogic/mappedbus),它允许多个Java进程(JVM)写入记录以便到同一个内存映射文件.
以下是Mappedbus如何解决多个编写器之间的同步问题:
该文件的前八个字节组成一个称为限制的字段.此字段指定实际已将多少数据写入文件.读者将轮询限制字段(使用volatile)以查看是否有要读取的新记录.
当编写者想要向文件添加记录时,它将使用fetch-and-add指令以原子方式更新限制字段.
当限制字段增加时,读者将知道要读取的新数据,但更新限制字段的编写者可能尚未在记录中写入任何数据.为避免此问题,每条记录都包含一个构成提交字段的初始字节.
当编写器完成写入记录时,它将设置提交字段(使用volatile),并且只有在看到已设置提交字段后,阅读器才会开始读取记录.
(顺便说一下,该解决方案仅经过验证,可以在Linux x86上使用Oracle的JVM.它很可能无法在所有平台上运行).
有几种方法。
\n\n让作者获得Lock尚未撰写的区域的独家报道。当所有内容都写入后释放锁。这与该系统上运行的所有其他应用程序兼容,但它要求读取器足够聪明,能够重试失败的读取,除非将其与其他方法之一结合起来
使用另一个通信通道,例如管道或套接字或 file\xe2\x80\x99s 元数据通道,让写入器告诉读取器已完成的写入。
在文件中的某个位置写入特殊标记(作为协议的一部分),说明写入的数据,例如
\n\nMappedByteBuffer bb;\n\xe2\x80\xa6\n// write your data\n\nbb.force();// ensure completion of all writes\nbb.put(specialPosition, specialMarkerValue);\nbb.force();// ensure visibility of the marker\nRun Code Online (Sandbox Code Playgroud)