Java核心基础加强系列-NIO初探

对于这部分内容其实我一直都是抗拒的,因为觉得非常难,但是没办法,对于我们开发者来说,不学习NIO是不可能的。就暂且非常粗浅地写下这篇文章。后面随着自己能力的提高,再去学习其更加核心的知识点和应用吧。

前言

Java NIO,Java New IO,也被称为Non-blocking IO,即非阻塞IO,是从Java 1.4开始引入的新的IO API,用以取代标准Java IO和网络API,当然,NIO的工作方式也与标准的IO API不同。NIO可以实现高性能的网络服务端程序,但是本篇文章暂不涉及到NIO的网络程序的开发,只关注其基础的部分,包括其核心的基础知识点,以及NIO对本地文件的处理,也会适时地将传统IO和NIO进行对比。

传统IO和NIO的区别

来看一张非常经典的图 io模型分类

这张图展示了几种IO模型的工作过程。我们可以重点看一下第一种和第二种,即Java中传统的IO和NIO。

从图中可以看出,其实一个完整的系统IO过程分为两个阶段:等待系统就绪和真正操作,举例来说,对于一个读操作,可以分为等待系统可读和实际地读。

需要注意的是,第一个等待系统就绪的阶段是不占用CPU的,CPU是处于“不干活”的状态,而第二个真正实际操作的阶段是占用CPU的,CPU是真正“干活”的状态。

阻塞与非阻塞

从图示可以看到,传统IO在两个阶段都是阻塞的,即传统IO中如果某个线程调用了read()或者write()方法,那么这个线程将会一直阻塞,除非线程完成这两个阶段的全部工作,否则线程一直是处于阻塞的状态,不能进行其他的操作。而我们知道在第一个阶段CPU是“不干活”的,是在“空等”,这样一直阻塞就会造成CPU的利用率低,为了解决这个问题,NIO应运而生。

NIO与传统IO最大的区别就是在第一个阶段,如果系统还未准备就绪,线程不会阻塞,会立即返回失败,如果系统准备就绪,直接将数据读取,返回给用户。而其实,第二阶段消耗的时间是非常短的,系统时间的消耗主要是在花费在第一阶段上。

流和缓冲区

在NIO出现以前,Java使用 的方式完成IO操作,所有的IO操作被视作单个字节的移动,NIO的出现改变了通过 完成IO的方式。

原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的,原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

面向流的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。

一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

NIO的核心知识点

网上随便一搜,大家应该都可以搜出来,NIO核心的点包括:通道(Channel),缓冲区(Buffer)和选择器(Selector),在这里我们暂不讨论选择器,因为选择器一般用在NIO的网络编程中,是实现高性能服务端程序的关键类。数据总是从通道读取到缓冲区,或者从缓冲区写入到通道 下面我们来对这些核心类进行详细的解释。

缓冲区(Buffer)

缓冲区实际就是一块内存区域,只是被包装成了缓冲区,其中包含了要写到通道或者刚从通道读取出来的的数据,缓冲区实际上是一个数组,一般情况下是字节数组,不过也可以是其他类型的数组,其提供了对数据的结构化访问,还可以跟踪系统的读/写进程。

缓冲区类型

最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。

ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:

ByteBuffer

CharBuffer

ShortBuffer

IntBuffer

LongBuffer

FloatBuffer

DoubleBuffer

每一个 Buffer 类都是 Buffer 接口的一个实例。 除了 ByteBuffer,每一个 Buffer 类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作。

重要属性和方法

  1. 容量capacity

缓冲区的容量很好理解,指的就是缓冲区能存储数据的总长度,例如:若缓冲区为ByteBuffer,那么容量就是指该缓冲区总共能存储容量capacity大小的字节。

  1. 位置position

这个属性,在不同的模式下具有不同的含义。

写模式下:position指的是可写位置的起点。

读模式下:position指的是可读位置的起点。

  1. 上限limit

写模式下:limit指的是可写位置的终点,即为capacity。

读模式下:limit指的是可读位置的终点。

  1. Flip()方法

将缓冲区切换为读模式,即将limit设置为position,将position设置为0,在从缓冲区中读取数据之前,必须调用该方法,否则将无法读取。

通道Channel

Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。

正如前面提到的,所有数据都通过 Buffer 对象来处理。永远不会将字节直接写入通道中,相反,是将数据写入包含一个或者多个字节的缓冲区。同样,不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这些数据。

通道类型

通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。

因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。

NIO实战

讲了这么多理论的东西,到底NIO要怎么处理文件呢?下面我们来实战。

NIO读取文件

大约分为以下几个步骤:

  1. 获取通道Channel。
  2. 分配缓冲区。
  3. 从通道中循环读取数据到缓冲区。 下面是一个可供参考的例子。
/**
 * Author: listeningrain
 * Date: 2019-02-19 16:57
 * Description: Nio读写文件测试
 */
public class NioTest {
    public static void main(String[] args){
         nioRead("/Users/jingxintingyu/Desktop/test.txt");
    }
    public static void nioRead(String path){
        try(FileInputStream fileInputStream = new FileInputStream(new File(path));
            //从输入流中获取相应的Channel
            FileChannel inputChannel = fileInputStream.getChannel();){      
            //分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //从通道从循环读取到缓冲区中
            while (-1 != inputChannel.read(buffer)){
                //将缓冲区的模式设置为读模式
                buffer.flip();
                //从缓冲区中读取数据 
                System.out.println(Charset.forName("UTF-8").decode(buffer));
                //清空缓冲区(注意:此步相当于将缓冲区的模式改为写模式)
                buffer.clear();
            }
        }catch (Exception e){
            e.fillInStackTrace();
            System.out.println("读取文件出现异常");
        }
    }
}

以下是运行结果:

hello world! nio

NIO写文件

/**
 * Author: listeningrain
 * Date: 2019-02-19 16:57
 * Description: Nio读写文件测试
 */
public class NioTest {
    public static void main(String[] args){
        //nioRead("/Users/jingxintingyu/Desktop/test.txt");
        nioWrite("/Users/jingxintingyu/Desktop/testOutOut.txt");
    }
    public static void nioWrite(String path){
        try (FileOutputStream fileOutputStream = new FileOutputStream(new File(path));
             //获取通道Channel
             FileChannel outputChannel = fileOutputStream.getChannel();){
            //分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //向buffer中放入数据
            buffer.put("hello world nio !".getBytes());
            //切换为读模式
            buffer.flip();
            //把数据从缓冲区写到通道
            outputChannel.write(buffer);
            //清空缓冲区
            buffer.clear();
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("写出文件异常");
        }
    }

NIO复制文件

/**
 * Author: listeningrain
 * Date: 2019-02-19 16:57
 * Description: Nio读写文件测试
 */
public class NioTest {
    public static void main(String[] args){
        //nioRead("/Users/jingxintingyu/Desktop/test.txt");
        //nioWrite("/Users/jingxintingyu/Desktop/testOutOut.txt");
        nioCopy("/Users/jingxintingyu/Desktop/test.txt","/Users/jingxintingyu/Desktop/test-copy.txt");
    }
    public static void nioCopy(String source, String target){
        try(FileInputStream fileInputStream = new FileInputStream(new File(source));
            FileChannel inputChannel = fileInputStream.getChannel();
            FileOutputStream fileOutputStream = new FileOutputStream(new File(target));
            FileChannel outputChannel = fileOutputStream.getChannel()){
            //分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //循环从通道中读取数据
            while(-1 != inputChannel.read(buffer)){
                //切换为读模式
                buffer.flip();
                //写到通道
                outputChannel.write(buffer);
                buffer.clear();
            }
        }catch (Exception e){
            System.out.println("复制文件出现异常");
        }
    }

传统IO和NIO在处理文件上的速度差异

既然NIO是用来取代传统IO的,那么我们就从处理文件的速度差异上来看看NIO到底有多快。

本次主要测试IO和NIO在复制文件上的速度差异,我们将两种方式的缓冲区大小均设置为1024个字节。 以下是测试的代码

/**
 * User:        sunqingfeng6
 * Date:        2019/2/19 10:10
 * Description: 传统IO和NIO在读取文件时的时间消耗
 */
public class TestTimeConsume {

    public static void main(String[] args){
        String path = "D:\\QQ_COIN.txt";
        io(path,"D:\\QQ_COIN_io.txt");
        nio(path,"D:\\QQ_COIN_nio.txt");
    }

    //传统IO
    private static void io(String path,String target){
        long start = System.currentTimeMillis();
        System.out.println("io开始复制文件---"+start);

        File file = new File(path);

        try(InputStream inputStream = new FileInputStream(file);
            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);

            OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(target))) {

            byte[] buffer = new byte[1024];
            int length;
            while((length = bufferedInputStream.read(buffer)) != -1){
                outputStream.write(buffer,0,length);
                outputStream.flush();
            }

        }catch (Exception e){
            System.out.println("读取文件异常");
        }
        long end = System.currentTimeMillis();
        System.out.println("io复制文件完毕---"+end);
        long ccc = end - start;
        System.out.println("io复制总耗时:"+ ccc);

    }

    //NIO方式
    private static void nio(String path,String target){
        long start = System.currentTimeMillis();
        System.out.println("nio开始复制文件---"+start);

        File file = new File(path);
        try(FileInputStream fileInputStream = new FileInputStream(file);
            FileOutputStream fileOutputStream = new FileOutputStream(new File(target))) {
            ByteBuffer allocate = ByteBuffer.allocate(1024);
            FileChannel inputChannel = fileInputStream.getChannel();
            FileChannel outputChannel = fileOutputStream.getChannel();
            while (-1 != inputChannel.read(allocate)){
                //将缓冲区切换为读模式
                allocate.flip();

                //System.out.print(Charset.forName("utf-8").decode(allocate));
                outputChannel.write(allocate);
                //相当于将缓冲区的模式改成了写模式
                allocate.clear();
            }
        }catch (Exception e){
            System.out.println("读取文件异常");
        }

        long end = System.currentTimeMillis();
        System.out.println("nio复制文件完毕---"+end);
        long ccc = end - start;
        System.out.println("nio复制总耗时:"+ ccc);
    }
}

先来测试拷贝一个大小为400MB的压缩包文件,以下是测试结果截图: 测试结果截图1

再来测试一个拷贝大小为14K的文本文件,以下是测试结果截图: 测试结果截图2

经过测试,复制后的文件均可以正常打开。

经过以上简单的测试,我们可以得出粗略的结论:小文件的处理,不适合使用NIO

暂无评论
发表新评论