Java NIO学习之FileChannel

概述

Java的FileChannel读文件方式相比于普通的IO流方式更高效,主要原因有以下几点:

 * FileChannel可以直接将文件映射到内存中,避免了不必要的数据拷贝,提高了读取文件的效率。
 *
 * FileChannel支持异步读取,可以在读取数据的同时进行其他操作,提高了程序的并发性能。
 *
 * FileChannel支持从文件的任意位置读取数据,而不像普通的IO流只能从文件的开头读取数据。
 *
 * FileChannel支持内存映射文件,可以将文件的一部分或全部映射到内存中,这样可以直接在内存中进行读写操作,避免了频繁的磁盘IO操作,提高了程序的性能。

读取数据

1. 单个缓冲区

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
ByteBuffer buf = ByteBuffer.allocate(5);
while(channel.read(buf)!=-1){
  buf.flip();
  System.out.print(new String(buf.array()));
  buf.clear();
}
channel.close();

2. 多个缓冲区

ScatteringByteChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
ByteBuffer key = ByteBuffer.allocate(5), value=ByteBuffer.allocate(10);
ByteBuffer[] buffers = new ByteBuffer[]{key, value};
while(channel.read(buffers)!=-1){
   key.flip();
   value.flip();
   System.out.println(new String(key.array()));
   System.out.println(new String(value.array()));
   key.clear();
   value.clear();
}
channel.close();

写入数据

1. 单个缓冲区

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
ByteBuffer buf = ByteBuffer.allocate(5);
byte[] data = "Hello, Java NIO.".getBytes();
for (int i = 0; i < data.length; ) {
  buf.put(data, i, Math.min(data.length - i, buf.limit() - buf.position()));
  buf.flip();
  i += channel.write(buf);
  buf.compact();
}
channel.force(false);
channel.close();

2. 多个缓冲区

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
ByteBuffer key = ByteBuffer.allocate(10), value = ByteBuffer.allocate(10);
byte[] data = "017 Robothy".getBytes();
key.put(data, 0, 3);
value.put(data, 4, data.length-4);
ByteBuffer[] buffers = new ByteBuffer[]{key, value};
key.flip();
value.flip();
channel.write(buffers);
channel.force(false); // 将数据刷出到磁盘
channel.close();

文件锁

可以通过调用 FileChannel 的 lock() 或者 tryLock() 方法来获得一个文件锁,获取锁的时候可以指定参数起始位置 position,锁定大小 size,是否共享 shared。如果没有指定参数,默认参数为 position = 0, size = Long.MAX_VALUE, shared = false。

位置 position 和大小 size 不需要严格与文件保持一致,position 和 size 均可以超过文件的大小范围。例如:文件大小为 100,可以指定位置为 200, 大小为 50;则当文件大小扩展到 250 时,[200,250) 的部分会被锁住。

shared 参数指定是排他的还是共享的。要获取共享锁,文件通道必须是可读的;要获取排他锁,文件通道必须是可写的。

由于 Java 的文件锁直接映射为操作系统的文件锁实现,因此获取文件锁时代表的是整个虚拟机,而非当前线程。若操作系统不支持共享的文件锁,即使指定了文件锁是共享的,也会被转化为排他锁。

FileLock lock = channel.lock(0, Long.MAX_VALUE, false);// 排它锁,此时同一操作系统下的其它进程不能访问 a.txt
System.out.println("Channel locked in exclusive mode.");
Thread.sleep(30 * 1000L); // 锁住 30 s
lock.release(); // 释放锁

lock = channel.lock(0, Long.MAX_VALUE, true); // 共享锁,此时文件可以被其它文件访问
System.out.println("Channel locked in shared mode.");
Thread.sleep(30 * 1000L); // 锁住 30 s
lock.release();

与 lock() 相比,tryLock() 是非阻塞的,无论是否能够获取到锁,它都会立即返回。若 tryLock() 请求锁定的区域已经被操作系统内的其它的进程锁住了,则返回 null;而 lock() 会阻塞,直到获取到了锁、通道被关闭或者线程被中断为止

映射文件到直接内存

1.FileChannel.map

文件通道 FileChanle 通过成员方法 map(MapMode mode, long position, long size) 将文件映射到应用内存。

FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE); // 以读写的方式打开文件通道
MappedByteBuffer buf = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()); // 将整个文件映射到内存
  • mode 表示打开模式,为枚举值,其值可以为 READ_ONLY, READ_WRITE, PRIVATE。

    模式为 READ_ONLY 时,不能对 buf 进行写操作;
    模式为 READ_WRITE 时,通道 fileChannel 必须具有读写文件的权限;对 buf 进行的写操作将对文件生效,但不保证立即同步到 I/O 设备;
    模式为 PRIVATE 时,通道 fileChannle 必须对文件有读写权限;但是对文件的修改操作不会传播到 I/O 设备,而是会在内存复制一份数据。此时对文件的修改对其它线程和进程不可见。

  • position 指定文件的开始映射到内存的位置;

  • size 指定映射的大小,值为非负 int 型整数。

调用 map() 方法之后,返回的 MappedByteBuffer 就与 fileChannel 脱离了关系,关闭 fileChannel 对 buf 没有影响。同时,如果要确保对 buf 修改的数据能够同步到文件 I/O 设备中,需要调用 MappedByteBuffer 中的无参数的 force() 方法,而调用 FileChannel 中的 force(metaData) 方法无效。

此时可以通过操作缓冲区来操作文件了。不过映射的内容存在于 JVM 程序的堆外内存中,这部分内存是虚拟内存,意味着 buf 中的内容不一定都在物理内存中,要让这些内容加载到物理内存,可以调用 MappedByteBuffer 中的 load() 方法。另外,还可以调用 isLoaded() 来判断 buf 中的内容是否在物理内存中。

FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ);
MappedByteBuffer buf = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
fileChannel.close();    // 关于文件通道对 buf 没有影响
System.out.println(buf.capacity()); // 输出 fileChannel.size()
System.out.println(buf.limit());    // 输出 fileChannel.size()
System.out.println(buf.position()); // 输出 0
buf.put((byte)'R'); // 写入内容
buf.compact();      // 截掉 positoin 之前的内容
buf.force();        // 将数据刷出到 I/O 设备

2.内存映射原理

通过 ByteBuffer.allocate() 分配的缓冲区是一个 HeapByteBuffer,存在于 JVM 堆中;而 FileChannle.map() 将文件映射到直接内存,返回的是一个 MappedByteBuffer,存在于堆外的直接内存中;这块内存在 MappedByteBuffer 对象本身被回收之前有效
在这里插入图片描述
前面使用堆缓冲区 ByteBuffer 和文件通道 FileChannel 对文件的操作使用的是 read()/write() 系统调用。读取数据时数据从 I/O 设备读到内核缓存,再从内核缓存复制到用户空间缓存,这里是 JVM 的堆内存。而映射磁盘文件是使用 mmap() 系统调用,将文件的指定部分映射到程序地址空间中;数据交互发生在 I/O 设备于用户空间之间,不需要经过内核空间。
在这里插入图片描述

小结

  1. 文件通道 FileChannel 能够将数据从 I/O 设备中读入(read)到字节缓冲区中,或者将字节缓冲区中的数据写入(write)到 I/O 设备中。

  2. 文件通道能够转换到 (transferTo) 一个可写通道中,也可以从一个可读通道转换而来(transferFrom)。这种方式使用于通道之间地数据传输,比使用缓冲区更加高效。

  3. 文件通道能够将文件的部分内容映射(map)到 JVM 堆外内存中,这种方式适合处理大文件,不适合处理小文件,因为映射过程本身开销很大。

  4. 在对文件进行重要的操作之后,应该将数据刷出刷出(force)到磁盘,避免操作系统崩溃导致的数据丢失。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值