JAVA NIO之文件通道
创建通道
FileChannel 是一个用于连接文件的通道,通过该通道,既可以从文件中读取,也可以向文件中写入数据。与SocketChannel 不同,FileChannel 无法设置为非阻塞模式,这意味着它只能运行在阻塞模式下。在使用FileChannel 之前,需要先打开它。由于 FileChannel 是一个抽象类,所以不能通过直接创建而来。必须通过像 InputStream、OutputStream 或 RandomAccessFile 等实例获取一个 FileChannel 实例。
FileInputStream fis = new FileInputStream(FILE_PATH); FileChannel channel = fis.getChannel(); FileOutputStream fos = new FileOutputStream(FILE_PATH); FileChannel channel = fis.getChannel(); RandomAccessFile raf = new RandomAccessFile(FILE_PATH , "rw"); FileChannel channel = raf.getChannel();
读写操作
代码会先向文件中写入数据,然后再将写入的数据读出来并打印。代码如下:
// 获取管道 RandomAccessFile raf = new RandomAccessFile(FILE_PATH, "rw"); FileChannel rafChannel = raf.getChannel(); // 准备数据 String data = "新数据,时间: " + System.currentTimeMillis(); System.out.println("原数据:\n" + " " + data); ByteBuffer buffer = ByteBuffer.allocate(128); buffer.clear(); buffer.put(data.getBytes()); buffer.flip(); // 写入数据 rafChannel.write(buffer); rafChannel.close(); raf.close(); // 重新打开管道 raf = new RandomAccessFile(FILE_PATH, "rw"); rafChannel = raf.getChannel(); // 读取刚刚写入的数据 buffer.clear(); rafChannel.read(buffer); // 打印读取出的数据 buffer.flip(); byte[] bytes = new byte[buffer.limit()]; buffer.get(bytes); System.out.println("读取到的数据:\n" + " " + new String(bytes)); rafChannel.close(); raf.close();
数据转移操作
需要将一个文件中的内容复制到另一个文件中去,最容易想到的做法是利用传统的 IO 将源文件中的内容读取到内存中,然后再往目标文件中写入。现在,有了 NIO,我们可以利用更方便快捷的方式去完成复制操作。FileChannel 提供了一对数据转移方法 - transferFrom/transferTo,通过使用这两个方法,即可简化文件复制操作。
public static void main(String[] args) throws IOException { RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); // 将 fromFile 文件找那个的数据转移到 toFile 中去 System.out.println("before transfer: " + readChannel(toChannel)); fromChannel.transferTo(position, count, toChannel); System.out.println("after transfer : " + readChannel(toChannel)); fromChannel.close(); fromFile.close(); toChannel.close(); toFile.close(); } private static String readChannel(FileChannel channel) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(32); buffer.clear(); // 将 channel 读取位置设为 0,也就是文件开始位置 channel.position(0); channel.read(buffer); // 再次将文件位置归零 channel.position(0); buffer.flip(); byte[] bytes = new byte[buffer.limit()]; buffer.get(bytes); return new String(bytes); }
可以明显感受到,利用 transferTo 减少了编码量。那么为什么利用 transferTo 可以减少编码量呢?在解答这个问题前,先来说说程序读取数据和写入文件的过程。
我们现在所使用的 PC 操作系统,将内存分为了内核空间和用户空间。操作系统的内核和一些硬件的驱动程序就是运行在内核空间内,而用户空间就是我们自己写的程序所能运行的内存区域。这里,当我们调用 read 从磁盘中读取数据时,内核会首先将数据读取到内核空间中,然后再将数据从内核空间复制到用户空间内。也就是说,我们需要通过内核进行数据中转。同样,写入数据也是如此。系统先从用户空间将数据拷贝到内核空间中,然后再由内核空间向磁盘写入。相关示意图如下:

与上面的数据流向不同,FileChannel 的 transferTo 方法底层基于 sendfile64(Linux 平台下)系统调用实现。sendfile64 会直接在内核空间内进行数据拷贝,免去了内核往用户空间拷贝,用户空间再往内核空间拷贝这两步操作,因此提高了效率。其示意图如下:

通过上面的讲解,大家应该知道了 transferTo 和 transferFrom 的效率会高于传统的 read 和 write 在效率上的区别。区别的原因在于免去了内核空间和用户空间的相互拷贝,虽然内存间拷贝的速度比较快,但涉及到大量的数据拷贝时,相互拷贝的带来的消耗是不应该被忽略的。
讲完了背景知识,咱们再来看看 FileChannel 是怎样调用 sendfile64 这个函数的。相关代码如下:
内存映射
内存映射这个概念源自操作系统,是指将一个文件映射到某一段虚拟内存(物理内存可能不连续)上去。我们通过对这段虚拟内存的读写即可达到对文件的读写的效果,从而可以简化对文件的操作。当然,这只是内存映射的一个优点。内存映射还有其他的一些优点,比如两个进程映射同一个文件,可以实现进程间通信。再比如,C 程序运行时需要 C 标准库支持,操作系统将 C 标准库放到了内存中,普通的 C 程序只需要将 C 标准库映射到自己的进程空间内就行了,从而可以降低内存占用。
Unix/Linux 操作系统内存映射的系统调用mmap,Java 在这个系统调用的基础上,封装了 Java 的内存映射方法。
// 从标准输入获取数据 Scanner sc = new Scanner(System.in); System.out.println("请输入:"); String str = sc.nextLine(); byte[] bytes = str.getBytes(); RandomAccessFile raf = new RandomAccessFile("map.txt", "rw"); FileChannel channel = raf.getChannel(); // 获取内存映射缓冲区,并向缓冲区写入数据 MappedByteBuffer mappedBuffer = channel.map(MapMode.READ_WRITE, 0, bytes.length); mappedBuffer.put(bytes); raf.close(); raf.close(); // 再次打开刚刚的文件,读取其中的内容 raf = new RandomAccessFile("map.txt", "rw"); channel = raf.getChannel(); System.out.println("\n文件内容:") System.out.println(readChannel(channel)); raf.close(); raf.close();
接下来在用 C 代码演示上面代码的功能,如下:
#include <stdio.h> #include <fcntl.h> #include <sys/mman.h> #include <memory.h> #include <unistd.h> int main() { int dstfd; void *dst; char buf[64], out[64]; int len; printf("Please input:\n"); scanf("%s", buf); len = strlen(buf); // 打开文件 dstfd = open("dst.txt", O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); lseek(dstfd, len - 1, SEEK_SET); write(dstfd, "", 1); // 将文件映射到内存中 dst = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, dstfd, 0); // 将输入的数据拷贝到映射内存中 memcpy(dst, buf, len); munmap(dst, len); close(dstfd); // 重新打开文件,并输出文件内容 dstfd = open("dst.txt", O_RDONLY); dst = mmap(NULL, len, PROT_READ, MAP_SHARED, dstfd, 0); bzero(out, 64); memcpy(out, dst, len); printf("\nfile content:\n%s\n", out); munmap(dst, len); close(dstfd); return 0; }
从上面的分析可以看出,NIO FileChannel 在实现上,实际上是对底层操作系统的一些 API 进行了再次封装,也就是一层皮。有了这层封装后,对上就屏蔽了底层 API 的细节,以降低使用难度。Java 为了提高开发效率,屏蔽了操作系统层面的细节。
参考:
http://www.tianxiaobo.com/2018/03/24/JAVA-NIO%E4%B9%8B%E6%96%87%E4%BB%B6%E9%80%9A%E9%81%93/
浙公网安备 33010602011771号