Java开发笔记(九十四)文件通道的性能优势
前面介绍了字节缓存的一堆概念,可能有的朋友还来不及消化,虽然文件通道的用法比起传统I/O有所简化,可是平白多了个操控繁琐的字节缓存,分明比较传统I/O更加复杂了。尽管字节缓存享有缓存方面的性能优势,但传统I/O也有缓存输入输出流呀,大家都有缓存机制,凭什么说NIO的文件处理更高效?之所以目前还看不出文件通道的性能优势,是因为前面介绍的仅限于它的基本用法,尚未涉及到高级特性,接下来阐述文件通道的真正杀手锏:使用通道复制文件。
复制文件的常规做法很简单,从源文件中读出数据,再将数据写进目标文件。采取文件通道和字节缓存的话,按照传统思路实现的文件复制代码示例如下:
// 使用文件通道和字节缓存复制文件 private static void copyChannelBuffer() { // 分别创建源文件的文件通道,以及目标文件的文件通道 try (FileChannel src = new FileInputStream(mSrcName).getChannel(); FileChannel dest = new FileOutputStream(mDestName).getChannel()) { int size = (int) src.size(); // 获取源文件的大小 // 分配指定大小的字节缓存 ByteBuffer buffer = ByteBuffer.allocateDirect(size); src.read(buffer); // 把源文件中的数据读到字节缓存 buffer.flip(); // 从缓冲区读取数据之前,必须先调用flip方法 dest.write(buffer); // 把字节缓存中的数据写入目标文件 } catch (Exception e) { e.printStackTrace(); } }
上述代码与缓存输入输出流的实现代码看起来半斤八两,似乎程序运行效率也差不了多少,然而事实上的性能差距可大了。虽然应用程序的代码像是能够直接读写文件,但是应用程序依附于操作系统,它发出的文件读写指令需要经由操作系统来完成。也就是说,应用程序从磁盘文件读取数据的流程实际上是这样的:磁盘文件→操作系统→应用数据;应用程序把内存数据写入磁盘文件的流程则是这样的:应用数据→操作系统→磁盘文件。注意操作系统和应用程序分配到的存储空间是不一样的,设备的内存在运行时被划分为系统内存与用户内存两大块,其中系统内存装载了系统程序及其使用的内存空间,剩下的用户内存才能依次分给每个应用作为应用程序自身的内存空间。譬如电脑开机之后,刚进入桌面尚未打开任何一个应用程序,电脑内存就已经被消耗了相当一大块,正是操作系统自行占据系统内存的缘故。于是操作系统收到读文件指令之后,先把磁盘文件的数据读到系统内存当中,然后才由应用程序把系统内存中的数据读到应用内存;写文件操作同理,应用程序先把内存数据写到系统内存,再由操作系统把系统内存中的数据写入磁盘文件。因此,传统IO复制文件的完整数据流程正如下图所示:
由图示可见,传统IO在复制文件的过程中一共花费了四个步骤,分别是:步骤①(磁盘文件→系统内存)、步骤②(系统内存→应用内存)、步骤③(应用内存→系统内存)、步骤③(系统内存→磁盘文件),这四个步骤跑下来,难怪传统IO的处理效率高不到哪里去。
使用文件通道就不一样了,通道本身是专门负责I/O操作的处理机,字节缓存又是通道内部的存储空间,故而利用通道复制文件的话,既无需动用操作系统的系统内存,也无需动用应用程序的应用内存。那么使用文件通道完成文件复制功能仅仅需要两个步骤,即先将磁盘上的原文件内容读到通道中的字节缓存,再将字节缓存中的数据写入磁盘上的新文件,更直观的数据流转过程如下图所示:
由上图可见,采用通道复制文件才花了有两个步骤:步骤①(磁盘文件→字节缓存)、步骤②(字节缓存→磁盘文件),显然通道复制的性能要优于传统IO了。
针对文件复制功能,由于已经明确要把源文件的全部内容完成写入新文件,因此不必显式通过字节缓存完成数据的读取与写入动作,可以直接调用通道对象的transferTo方法或者transferFrom方法完成文件复制。其中transferTo方法操作的是源文件通道,它把数据传给目标文件通道;transferFrom方法操作的是目标文件通道,它从源文件通道传入数据。详细的调用代码例子如下所示:
// 使用文件通道直接复制文件 private static void copyChannelDirect() { // 分别创建源文件的文件通道,以及目标文件的文件通道 try (FileChannel src = new FileInputStream(mSrcName).getChannel(); FileChannel dest = new FileOutputStream(mDestName).getChannel();) { // 下面的transferTo和transferFrom都可以完成文件复制功能,选择其中一个即可 src.transferTo(0, src.size(), dest); // 操作源文件通道,把数据传给目标文件通道 //dest.transferFrom(src, 0, src.size()); // 操作目标文件通道,从源文件通道传入数据 } catch (Exception e) { e.printStackTrace(); } }
更多Java技术文章参见《Java开发笔记(序)章节目录》