NIO 的使用

98 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

NIO 介绍

由于传统的 IO 流在实际运行时效率较低,所以 JDK 4 版本中提出了 NIO 技术,带来了可观的性能提升。

NIO :Non-Blocking IO,指的是非阻塞的 IO 。

传统的流式 IO ,又可以称为 BIO (Blocking IO) 阻塞 IO。

如何理解阻塞与非阻塞呢?

比如私家车和出租车,私家车买回来后只能是车主一家可以使用,其他不相干的人是不能使用这辆车的,这样私家车处于阻塞状态,车门锁上其他人是不能进入的;而出租车整日都是在路上穿行,有客人它就载客,到达目的客人下车,恢复空车状态后继续揽客,这样出租车便处于非阻塞状态,有客人时便可打开车门进行乘坐。显然阻塞状态存在资源极大浪费,而非阻塞状态可以充分发挥物尽其用的原则。

所以 NIO 的提出在文件的传输过程中极大的提高了传输的性能。

为什么提出

我们学习了 IO 流,应该知道分为输入流和输出流,输入与输出都有相对应的类进行处理,如,InputStream 类只能读文件,OutputStream 类只能写文件,两者是不能互换的,它们各司其职。

如果现在打开一个文件,想要一会写一会读,操作起来非常麻烦,读的时候需要 InputStream 的全套操作,写的时候需要 OutputStream 的全套操作,这样就忙坏了输入和输出流了。

因此,我们需要引入 NIO 提供的 文件通道,文件通道中的数据可以双向流动,也就是说流进来便是读操作,流出去便是写操作,这样文件的读写操作可以在文件通道中进行,大大节省了系统资源的开销。

文件通道 FileChannel

Java 中提供的文件通道 FileChannel 类,存放在 java.nio.channels 包中。

🤔 那么如何获取 FileChannel 的实例对象呢?

有两种方式可以获取 FileChannel 实例。

👉 方式一:通过文件字节输入输出流中的 getChannel() 方法来获取。

FileChannel inputchannel = new FileInputStream(文件名).getChannel();
FileChannel outputchannel = new FileOutputStream(文件名).getChannel();

👉 方式二:通过随机访问文件工具 RandomAccessFile 类中的 getChannel() 方法来获取。

// 获取可读的文件通道
FileChannel inputChannel = new RandomAccessFile(文件名, "r").getChannel();
// 获取可写的文件通道
FileChannel outputChannel = new RandomAccessFile(文件名, "rw").getChannel();

文件通道实例获取后,便可以通过类中的方法进行数据交互了。

FileChannel 类常用的方法,如下表:

方法名说明
isOpen判断文件通道是否打开。
size获取文件通道中的文件大小。
truncate把文件大小截取到指定长度。
read把文件通道中的数据读到字节缓存。
write往文件通道写入字节缓存中的数据。
force强制写入,类似于缓冲流中的 flush 方法。
close关闭文件通道。

文件通道中的读写操作,主要是通过 read() 和 write() 方法进行处理,而文件通道中的读写操作需要有存储空间,这个存储空间就是字节缓存。

字节缓存 ByteBuffer

文件通道的读写操作,在通道内需要有内部的存储空间进行存储操作,它是缓存 Buffer 类提供的。

Buffer 类存放在 Java API 的 java.nio 包中。

Buffer 类也是一个抽象类,主要缓存空间的使用,是采用它的子类来进行操作。

Buffer 缓存提供了:字节缓存、字符缓存、双精度缓存、单精度缓存、整型缓存、长整型缓存、短整型缓存,这些子类同样也都是抽象类。

👉 Buffer 类中提供常用方法有 3 个,如下:

  1. clear() :清空缓冲区的数据。
  2. flip() :把缓冲区从写模式切换到读模式。读取数据前,需要先调用 flip 方法。
  3. rewind() :让缓冲区的指针回到开头,重新在读一遍。

ByteBuffer 字节缓存,它是一种特殊的存储空间,在操作中可以被多次读写。

👉 ByteBuffer 提供了几个构建方法:

静态方法说明
ByteBuffer.wrap(byte[] array)根据输入的字节数组生成对应的缓存对象。
ByteBuffer.wrap(byte[] array, int offset, int length)根据输入的字节数组开始和长度生成对应的缓存对象。
ByteBuffer.allocate(int capacity)根据输入的容量分配指定大小的新缓存。它将有一个后备数组,其数组偏移量将为零。
ByteBuffer.allocateDirect(int capacity)根据输入的容量分配指定大小的新缓存。

获取 ByteBuffer 对象实例后,也可以通过 get() 和 put() 方法存储数据。

实验

通过文件通道写入数据

第一步:创建文件输出流对象 FileOutputStream fos = new FileOutputStream(filename);

第二步:通过文件输出流对象获取文件通道对象 FileChannel channel = fos.getChannel();

第三步:确定写入的数据,String str = "xxxx";

第四步:生成字符串对应的字节缓存对象 ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());

第五步:往文件通道写入字节缓存 channel.write(buffer);

最后:资源释放 fos.close(); channel.close();

同理可以知道通过文件通道读取文件

第一步:创建文件输入流对象 FileInputStream fis = new FileInputStream(filename);

第二步:通过文件输入流对象获取文件通道对象 FileChannel channel = fis.getChannel();

第三步:获取文件通道中文件大小 int size = (int) channel.size();

第四步:根据文件大小创建新的字节缓存 ByteBuffer buffer = ByteBuffer.allocateDirect(size);

第五步:把文件通道中的数据读到字节缓存 channel.read(buffer);

第六步:把缓冲区切换到读模式,必须调用 flip 方法 buffer.flip();

第七步:创建与文件大小相同长度的字节数组 byte[] bytes = new byte[size];

第八步:把字节缓存中的数据读取到字节数组 buffer.get(bytes);

第九步:把字节数组转换成字符串 String content = new String(bytes);

第十步:打印输出字符串信息 System.out.println(content);

最后:资源释放 fis.close(); channel.close();

总体代码:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class TestChannel{
    public void writeChannel(String filename) throws Exception{
        FileOutputStream fos=new FileOutputStream(filename);
        FileChannel channel=fos.getChannel();
        String str = "明月几时有?把酒问青天。不知天上宫阙,今夕是何年。\n我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间。\n转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆?\n人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共婵娟。";
        ByteBuffer buffer=ByteBuffer.wrap(str.getBytes());
        channel.write(buffer);
        fos.close();
        channel.close();
    }
    public void readChannel(String filename) throws Exception{
        FileInputStream fis=new FileInputStream(filename);
        FileChannel channel=fis.getChannel();
        int size=(int)channel.size();
        ByteBuffer buffer=ByteBuffer.allocateDirect(size);
        channel.read(buffer);
        buffer.flip();
        byte[] bytes=new byte[size];
        buffer.get(bytes);
        String content=new String(bytes);
        System.out.println(content);
        fis.close();
        channel.close();
    }
    public static void main(String[] args) {
        TestChannel tc = new TestChannel();
        try {
            tc.writeChannel("myfile.txt");
            tc.readChannel("myfile.txt");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

文件通道的性能优势

文件通道的性能优势主要体现在文件复制,而且越大的文件越能体现它的优势。

一般来说,文件传输最好采用字节流,使用字节流可以传输任何类型文件,不会被破坏任何格式,但是以字节为单位进行传输效率非常的低。

在这样的情况下,至少你应该想到采用缓冲流 BufferedInputStream 和 BufferedOutputStream 进行文件复制。

而今,大数据信息时代,高效操作时王道。所以采用文件通道进行文件复制,可以让效率完全体现。

⭐ 注意:当传输文件不大时,也无需使用文件通道,就好比杀鸡用牛刀,也是一种资源浪费。

传统 IO 流和文件通道复制文件的区别。

传统 IO 复制文件流程图如下:

图片描述

分为 4 步完成复制:

① 磁盘文件 --> 系统内存

② 系统内存 --> 应用内存

③ 应用内存 --> 系统内存

④ 系统内存 --> 磁盘文件

操作系统收到读文件指令后,把磁盘文件数据读到系统内存中,然后由应用程序把系统内存中的数据读到应用内存;写文件指令收到后,应用程序先把应用内存中的数据写到系统内存,再由操作系统把系统内存中的数据写入磁盘文件。

文件通道复制文件流程图如下:

图片描述

只需 2 步完成复制:

① 磁盘文件 --> 字节缓存

② 字节缓存 --> 磁盘文件

字节缓存是通道内部的存储空间,因此使用文件通道复制文件,无须动用系统内存,也无须使用应用内存,只需将磁盘上的文件内容读到通道中的字节缓存中,再将字节缓存中的数据写入磁盘上的新文件即可。

显然,文件通道复制文件性能优于传统 IO 。

文件复制也可以采用文件通道中的 transferTo 和 transferFrom 方法进行复制操作。