RandomAccessFile、FileChannel和MappedByteBuffer

一、RandomAccessFile

简单示例:

import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * @author debo
 * @date 2020-06-27
 */
public class RandomAccessFileTest {

    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("/home/debo/tmp.txt", "rw");
        String msg = "你好,world!";
        System.out.println("初始文件指针位置:" + raf.getFilePointer());
        raf.write(msg.getBytes());
        System.out.println("第一次写完之后文件指针位置:" + raf.getFilePointer());
        // 手动修改读写指针位置
        raf.seek(9);
        System.out.println("修改后的文件指针位置:" + raf.getFilePointer());
        byte b = raf.readByte();
        System.out.println("第一次读完之后文件指针位置:" + raf.getFilePointer() + ",读到的内容:" + (char) b);
        // 修改指针位置10处的字节内容
        raf.writeBytes("K");
        System.out.println("第二次写完之后文件指针位置:" + raf.getFilePointer());
        byte[] dst = new byte[(int) raf.length()];
        // 指针归零
        raf.seek(0);
        // 将数据全部读取出来
        raf.read(dst);
        System.out.println("第二次读完之后文件指针位置:" + raf.getFilePointer() + ",读到的内容:" + new String(dst));
        raf.close();
    }
}

输出如下:

初始文件指针位置:0
第一次写完之后文件指针位置:15
修改后的文件指针位置:9
第一次读完之后文件指针位置:10,读到的内容:w
第二次写完之后文件指针位置:11
第二次读完之后文件指针位置:15,读到的内容:你好,wKrld!

对比流式IO,随机访问IO的优势还是很明显的,通过移动读写指针,可以读任意位置和写任意位置,非常灵活。还有一个相比流式IO重要的区别是,用 FileOutputStream 去写文件时,如果文件已存在,则会删掉原有的文件或者在原有的文件末尾追加数据,而如果使用 RandomAccessFile 去写文件,如果文件已存在,依然可以通过移动读写指针来操纵已存在文件的任意位置。可以想象一下,如果已存在文件大小是150B,现在要将新内容写入指针位置15~30区间上,这个过程就好像是在编辑文件,只修改指定位置处的数据,而其它位置的数据保持不变。

我们知道,在写文件时,对文件的修改不会被立即写入到磁盘,而是先修改文件在内核的 page cache,操作系统会在合适的时机自动将修改同步到磁盘,这样就有可能会出现断电以后数据丢失的情况,为了进行更加严谨的数据控制,可以在每次 RandomAccessFile.write 之后调用 RandomAccessFile.getFD().sync() 强制将修改同步写入到磁盘。

RandomAccessFile 的访问模式有以下几种:

  • r:表示以只读方式打开文件
  • rw:表示以读写方式打开文件
  • rws:表示以读写方式打开文件,同时每次对文件的修改都会被同步写入磁盘
  • rwd:表示以读写方式打开文件,同时每次对文件内容的修改都会被同步写入磁盘,和rws的区别在于,rws模式下,会将文件内容和文件元数据的修改都同步到磁盘。

rws 和 rwd 模式下,每一次对文件的写操作都会等待同步到磁盘后才返回,不需要再额外调用 RandomAccessFile.getFD().sync() 方法,但是很显然没有 rw 模式下再手动调用同步刷盘来得灵活。

二、FileChannel

FileChannel 的功能和 RandomAccessFile 的功能大体一致,都通过文件位置指针来控制对文件的随机读写,FileChannel 实例可以通过 RandomAccessFile.getChannel()、FileInputStream.getChannel()、FileOutputStream.getChannel() 获取,JDK1.7之后还可以通过 FileChannel.open() 静态方法获取。简单示例如下:

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @author debo
 * @date 2020-06-27
 */
public class FileChannelTest {

    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("/home/debo/tmp.txt", "rw");
        FileChannel fileChannel = raf.getChannel();
        String msg = "你好,world!";
        System.out.println("初始文件指针位置:" + fileChannel.position());
        fileChannel.write(ByteBuffer.wrap(msg.getBytes()));
        System.out.println("第一次写完之后文件指针位置:" + fileChannel.position());
        // 手动修改读写指针位置
        fileChannel.position(9);
        System.out.println("修改后的文件指针位置:" + fileChannel.position());
        ByteBuffer dst = ByteBuffer.allocate(1);
        fileChannel.read(dst);
        dst.flip();
        System.out.println("第一次读完之后文件指针位置:" + fileChannel.position() + ",读到的内容:" + (char) dst.get());
        // 修改指针位置10处的字节内容
        fileChannel.write(ByteBuffer.wrap("K".getBytes()));
        System.out.println("第二次写完之后文件指针位置:" + fileChannel.position());
        dst = ByteBuffer.allocate((int) fileChannel.size());
        // 指针归零
        fileChannel.position(0);
        // 将数据全部读取出来
        fileChannel.read(dst);
        dst.flip();
        System.out.println("第二次读完之后文件指针位置:" + fileChannel.position() + ",读到的内容:" + new String(dst.array()));
        fileChannel.close();
    }
}

输出如下:

初始文件指针位置:0
第一次写完之后文件指针位置:15
修改后的文件指针位置:9
第一次读完之后文件指针位置:10,读到的内容:w
第二次写完之后文件指针位置:11
第二次读完之后文件指针位置:15,读到的内容:你好,wKrld!

同样,FileChannel 也提供了手动刷盘的方法 FileChannel.force 将 page cache 中的文件修改同步写入到磁盘。

需要注意的是,通过 FileInputStream 获取的 FileChannel 不支持写操作,通过 FileOutputStream 获取的 FileChannel 不支持读操作,通过 “r” 模式的 RandomAccessFile 获取的 FileChannel 不支持写操作等等,这些都是在转换 FileChannel 时的应有之义。

三、MappedByteBuffer

mmap系统调用可以将文件的一个指定区域直接映射到用户进程的虚拟地址空间,这样当用户进程操作文件时,就像操作分配给自己的内存一样。更详细地说,就是以普通方式去读写文件时,会产生 read/write 系统调用,而通过 mmap 方式操作文件时,在文件读写的过程中不会产生 read/write 系统调用。

java 中使用 MappedByteBuffer 来表示这块内存映射区域,这个类的实例通过 FileChannel.map() 方法获取,示例如下:

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;

/**
 * @author debo
 * @date 2020-06-27
 */
public class MappedByteBufferWriteTest {
    private static final long COUNT = 1000_0000L;

    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("/home/debo/tmp.txt", "rw");
        FileChannel fileChannel = raf.getChannel();
        String msg = "你好,world!";
        // 内存映射区域总大小
        long size = msg.getBytes().length * COUNT;
        long start = System.currentTimeMillis();
        MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0, size);
        for (int i = 0; i < COUNT; i++) {
            map.put(msg.getBytes());
        }
        System.out.println("当前文件指针位置:" + map.position());
        raf.close();
        System.out.println(String.format("耗时:%d毫秒", System.currentTimeMillis() - start));
    }
}

mmap 同样支持通过文件指针随机读写文件,类似地,mmap也提供了手动刷盘的方法 MappedByteBuffer.force()。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值