Java> Java核心卷读书笔记 - 内存映射文件

简介

内存映射文件是操作系统利用内存,来实现将一个文件或者文件的一部分“映射”到内存中的文件。内存映射文件可当做数组访问,速度比传统文件访问快。

内存映射文件有何意义?
下图是一组测试数据,测试内容是对JDK的jre/lib中37MB rt.jar计算校验和CRC32所需时间。

可以明显看到,内存映射文件比随机访问RandomAccessFile要快很多,不过对比带缓冲的输入流优势不是很明显。

如何进行映射?

  1. 从文件中获得一个通道(channel),通道是用于磁盘文件的一种抽象,使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。
FileChannel channel = FileChannel.open(path, options);

// option 也可以缺省, 因为对缓冲区的读写由映射到缓冲区时传入FileChannel.MapMode参数决定
FileChannel channel = FileChannel.open(Paths.get("rss", "test.txt", StandardOpenOption.READ)); 
  1. 通过FileChannel类的map方法,从这个通道获取一个MappedByteBuffer,可以想要映射的文件区域(起始位置、元素个数)和映射模式。
    支持的映射模式
映射模式 描述 备注
FileChannel.MapMode.READ_ONLY 所产生的缓冲区只读,任何写缓冲区操作将导致ReadOnlyBufferException
FileChannel.MapMode.READ_WRITE 所产生的缓冲区可读写,任何修改都会在某个时刻写回文件中 多个程序同时进行文件映射的确切行为依赖于操作系统
FileChannel.MapMode.PRIVATE 多产生的缓冲器可读写,不过修改是私有的,不会影响到文件
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, (int)channel.size());
  1. 通过MappedByteBuffer类的buffer对象,读写数据
    注意:MappedByteBuffer缓冲区支持顺序访问和随机访问。

访问内存映射文件缓冲区

通过MappedByteBuffer读写缓冲区
顺序访问示例

while(buffer.hasRemaining()) {
      byte b = buffer.get();
      ...
}

随机访问示例

for(int i = 0; i < buffer.limit(); i ++) {
      byte b = buffer.get(i);
      ...
}

示例

示例代码

比较InputStream(普通输入流), BufferedInputStream(带缓冲的输入流), RandomAccessFile(随机访问文件),MappedByteBuffer(内存映射文件)这几种方式进行文件校验和CRC32计算时间。

    // InputStream 计算校验和
    public static long checksumInputStream(Path fileName) throws IOException{
        try (InputStream in = Files.newInputStream(fileName)) {
            CRC32 crc = new CRC32();

            int c;

            while ((c = in.read()) != -1) {
                crc.update(c);
            }

            return crc.getValue();
        }
    }

    // BufferedInputStream 计算校验和
    public static long checksumBufferedInputStream(Path filename) throws IOException {
        try(InputStream in = new BufferedInputStream(Files.newInputStream(filename))) {
            CRC32 crc = new CRC32();

            int c;
            while ((c = in.read()) != -1) {
                crc.update(c);
            }

            return crc.getValue();
        }
    }

    // RandomAccessFile 计算校验和
    public static long checksumRandomAccessFile(Path filename) throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(filename.toFile(), "r")) {
            long length = file.length();
            CRC32 crc = new CRC32();

            for (int i = 0; i < length; i++) {
                file.seek(i);
                int c = file.readByte();
                crc.update(c);
            }

            return crc.getValue();
        }
    }

    // MappedByteBuffer 计算校验和
    public static long checksumMappedFile(Path filename) throws IOException {
        try(FileChannel channel = FileChannel.open(filename)) {
            CRC32 crc = new CRC32();
            int length = (int)channel.size();
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length);

            for (int i = 0; i < length; i++) {
                int c = buffer.get(i);
                crc.update(c);
            }
            return crc.getValue();
        }
    }

    public static void main(String[] args) throws IOException {
        System.out.println("Input Stream:");
        long start = System.currentTimeMillis();
        Path filename = Paths.get("rss", "rt.jar");
        long crcValue = checksumInputStream(filename);
        long end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + "ms");

        System.out.println("Buffer Stream:");
        start = System.currentTimeMillis();
        crcValue = checksumBufferedInputStream(filename);
        end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + "ms");

        System.out.println("Random Access File:");
        start = System.currentTimeMillis();
        crcValue = checksumRandomAccessFile(filename);
        end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + "ms");

        System.out.println("Mapped File:");
        start = System.currentTimeMillis();
        crcValue = checksumMappedFile(filename);
        end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + "ms");
    }

示例运行结果

结果如下,可以看到运行与开始性能对比截图一致。 这也暗示着,对于追求处理速度的大文件,不建议使用InputStream和RandomAccessFile, 建议使用MappedByteBuffer或者BufferedInputStream。

Input Stream:
d6b12853
158067ms
Buffer Stream:
d6b12853
385ms
Random Access File:
d6b12853
181429ms
Mapped File:
d6b12853
203ms

文件加锁

锁定文件

使用FileChannel.lock()或者FileChannel.tryLock()。文件锁定后,将保持锁定, 直到通道关闭或者锁上调用了release()方法

FileChannel channel = FileChannel.open(path);
// 加锁方式1
FileLock lock = channel.lock(); // 会阻塞直至可获得锁

// 加锁方式2
FileLock lock = channel.tryLock(); // 立即返回, 要么返回锁, 要么不可获得锁时返回null. 

锁定文件的一部分

/**
* shared: false表示这是独占锁, 锁定文件的目的是读写; true表示这是一个共享锁, 允许多个进程从文件读入, 并阻止任何进程获得独占的锁. 不是所有操作系统都支持共享锁
*/
FileLock lock(long start, long size, boolean shared)
// or
FileLock tryLock(long start, long size, boolean shared)

// 查询支持的锁类型
FileLock.isShared();

如果锁定尾部,而文件后来长度增长超过锁定部分,那么增长出来的区域是未锁定的,要想锁住所有字节,使用Long.MAX_VALUE来表示尺寸。
例如,
FileLock lock = channel.lock(0, Long.MAX_VALUE, true);

释放锁

确保操作完成时释放锁,最好使用try语句

try (FileLock lock = channel.lock()) {
      access the locked file segment
}

文件加锁机制依赖于操作系统,需要注意:

  1. 某些系统中,文件加锁仅仅是建议。如果一个应用未得到锁,仍可以向被另一个应用并发锁定的文件执行写操作;
  2. 某些系统中,不能锁定一个文件的同时,将其映射到内存中;
  3. 文件锁是由整个Java虚拟机持有。如果2个程序是由同一个虚拟机启动,那么它们不可能每个都同时获得同一个文件上的锁。如果虚拟机已经在同一个文件上持有了另一个重叠的锁,那么这2个方法将抛出OverlapingFileLockException;
  4. 在一些系统中,关闭一个通道会释放由Java虚拟机持有的底层文件的所有锁。因此,同一个锁定文件上,应避免使用多个通道;
  5. 在网络文件系统上锁定文件是高度依赖于系统的,应尽量避免;
posted @ 2020-11-28 20:32  明明1109  阅读(139)  评论(0编辑  收藏  举报