NIO 的使用

Posted 猪八戒1.0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NIO 的使用相关的知识,希望对你有一定的参考价值。

知识点
FileChannel
ByteBuffer

一.介绍

由于传统的 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 包中。

我们可以查看 Java API ,发现 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 个,如下:

clear() :清空缓冲区的数据。
flip() :把缓冲区从写模式切换到读模式。读取数据前,需要先调用 flip 方法。
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() 方法存储数据。

接下来,我们通过代码来感受一下,如何使用 FileChannel 和 ByteBuffer 进行文件读写操作。

 

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
 * 文件通道的使用
 * @author 小桃子
 */
public class TestChannel 
    /**
     * 通过文件通道写入数据
     * @param filename 文件名
     * @throws IOException IO异常
     */
    public void writeChannel(String filename) throws IOException 
        // 创建文件输出流对象
        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();
    
    /**
     * 通过文件通道读取文件
     * @param filename 文件名
     * @throws IOException IO异常
     */
    public void readChannel(String filename) throws IOException 
        // 创建文件输入流对象
        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();
    

    public static void main(String[] args) 
        TestChannel tc = new TestChannel();
        try 
            tc.writeChannel("myfile.txt");
            tc.readChannel("myfile.txt");
         catch (IOException e) 
            e.printStackTrace();
        
    

四.文件通道的性能优势

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

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

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

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

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

/**
 * 通过文件通道完成文件复制操作
 * @param srcfile 源文件
 * @param destfile 目标文件
 * @return 是否完成复制操作
 */
public boolean copyFileChannel(String srcfile,String destfile) 
    boolean result = false;
    // 声明文件输入流
    FileInputStream input = null;
    // 声明输入文件通道
    FileChannel inputChannel = null;
    // 声明文件输出流
    FileOutputStream output = null;
    // 声明输出文件通道
    FileChannel outputChannel = null;

    try 
        // 创建文件输入流对象
        input = new FileInputStream(srcfile);
        // 获取输入文件通道对象
        inputChannel = input.getChannel();
        // 创建文件输出流对象
        output = new FileOutputStream(destfile);
        // 获取输出文件通道对象
        outputChannel = output.getChannel();

        // 根据读取文件的大小创建字节缓存对象
        ByteBuffer buffer = ByteBuffer.allocate((int)inputChannel.size());
        // 通过输入文件通道读取源文件,指定放入buffer字节缓存中
        inputChannel.read(buffer);
        // 将读取的数据存储到字节缓存中
        buffer.flip();
        // 将字节缓存中的数据写入输出文件通道指定目标文件中
        outputChannel.write(buffer);
        // 完成复制后返回 true,有任何异常发生都返回 false
        result = true;
     catch (FileNotFoundException e) 
        // 找不到 srcfile 会捕获的异常
        e.printStackTrace();
     catch (IOException e) 
        // 读取和写入数据的过程中出现的流异常
        e.printStackTrace();
     finally 
        // 资源释放
        try 
            // 输出文件通道关闭
            if(outputChannel!= null) 
                outputChannel.close();
            
            // 输出流关闭
            if(output != null) 
                output.close();
            
         catch (IOException e) 
            e.printStackTrace();
        
        try 
            // 输入文件通道关闭
            if(inputChannel != null) 
                inputChannel.close();
            
            // 输入流关闭
            if(input != null) 
                input.close();
            
         catch (IOException e) 
            e.printStackTrace();
        
    
    return result;

 五.区别

我们强调一下传统 IO 流和文件通道复制文件的区别。

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

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

 

 

以上是关于NIO 的使用的主要内容,如果未能解决你的问题,请参考以下文章

NIO与传统IO的区别

NIO与传统IO的区别

Webflux + Netty NIO 性能相比传统 IO 下降约 30 倍

详解 Java NIO

NIO

NIO 的使用