面试必备基础知识 — NIO

文章目录

  • 概述
  • 流 与 块
  • 通道与缓冲区
  • 选择器

概述

NIO 新的输入/输出库,是在 JDK1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准的Java代码中提供了 高速的、面向块的I/O。

NIO的创建目的 是为了让Java程序员可以实现高速I/O而无需编译自定义的本机代码。NIO将最耗时的 I/O 操作(填充和提取缓冲区)转移回操作系统,因而可以极大的提高速度。

NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。

流 与 块

I/O 与 NIO 最重要的区别就是数据打包和传输的方式I/O 以流的方式处理数据,而NIO 以块的方式处理数据。

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

面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要块的多。但是面向快的 I/O 缺少一些面向流的 I/O 所具有的简单性。

JDK 1.4 中原来的 I/O包和NIO 已经很好地集成了。
java.io.* 已经以NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.* 包中一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会很快。

通道与缓冲区

通道缓冲区 是NIO 中的核心对象,几乎每一个 I/O 操作都要使用它们。

通道是对原 I/O 包中流的模拟,到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。一个Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中。同样地,从通道中读取的任何数据都要读到缓冲区中。

通道

Channel 是一个对象,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动,而通道是双向的,可以同于读、写 或者 同时用于读写。

注意:一个流必须是 InputStream 或者 OutputStream 的子类

通道包含一下类型:

  • FileChannel:从文件中读写数据
  • DatagramChannel:通过 UDP 读写网络中的数据
  • SocketChannel:通过TCP 读写网络中的数据
  • ServerSocketChannel:可以监听新进来的TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel

缓冲区

Buffer 是一个对象,它包含一些要写入或者刚读出的数据。在 NIO 中加入 Buffer对象,体现了新库与 原来I/O 的一个重要区别。
在面向流的 I/O 中,我们是将数据直接写入或者将数据直接读到 Stream 对象中。
在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读取到缓冲中的,在写入数据时,也是写入到缓冲区中的。任何时候访问 NIO 中的数据,我们都是将它放到缓冲区中的。

缓冲区实质上是一个数组。 通常它是一个字节数组,但是也可以使用其他类型的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

缓冲区包括一下类型

对于每一种基本 Java 类型都有一种缓冲区类型:

  • ByteBuffer(最常用的缓冲区类型)
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

缓冲区的状态变量

  • position :当前已读写的字节数(位置)
  • limit:还可以读写的字节数(限制)
  • capacity :最大容量

这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。

状态变量改变过程举例

文件NIO实例

使用 NIO 快速复制文件:

public static void fastCopy(String src, String dist) throws IOException {

    /* 获得源文件的输入字节流 */
    FileInputStream fin = new FileInputStream(src);

    /* 获取输入字节流的文件通道 */
    FileChannel fcin = fin.getChannel();

    /* 获取目标文件的输出字节流 */
    FileOutputStream fout = new FileOutputStream(dist);

    /* 获取输出字节流的文件通道 */
    FileChannel fcout = fout.getChannel();

    /* 为缓冲区分配 1024 个字节 */
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

    while (true) {

        /* 从输入通道中读取数据到缓冲区中 */
        int r = fcin.read(buffer);

        /* read() 返回 -1 表示 EOF */
        if (r == -1) {
            break;
        }

        /* 切换读写 */
        buffer.flip();

        /* 把缓冲区的内容写入输出文件中 */
        fcout.write(buffer);

        /* 清空缓冲区 */
        buffer.clear();
    }
}

选择器

NIO 通常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。

NIO 实现类 I/O 多路复用中的 Reactor模型 ,一个线程Thread 使用一个选择器Selector 通过轮询的方式去监听多个通道Channel 上的事件,从而让一个线程就可以处理多个事件。

通过配置监听器的通道Channel为非阻塞,那么当Channel上的 I/O 事件还未到达时,就不会进入阻塞状态,而是继续轮询其它的Channel,找到 I/O事件已经到达的Channel执行。

因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件,而不是一个线程处理一件事,对于 I/O密集型的应用具有很好地性能。

创建选择器

Selector selector = Selector.open();

将通道注册到选择器中

为了接收连接,我们需要一个 ServerSocketChannel。事实上,我们要监听的每一个端口都需要有一个 ServerSocketChannel 。

//打开一个ServerSocketChannel
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//将ServerSocketChannel设置为非阻塞的
ssChannel.configureBlocking(false);
//将ServerSocketChannel注册到selector上
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。

在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。

监听事件

int num = selector.select();

这个方法会一直阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。

获取到达的事件

//selectedKeys()方法 返回发生了事件的 SelectionKey 对象的一个集合 。
//一个SelectionKey表示一个到达的事件
Set<SelectionKey> keys = selector.selectedKeys();

//通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    //对于每一个 SelectionKey, 必须确定发生的是什么 I/O 事件
    //以及这个事件影响哪些 I/O 对象。
    if (key.isAcceptable()) {
        // ...
    } else if (key.isReadable()) {
        // ...
    }
    keyIterator.remove();
}

NIO 与普通 I/O 的区别主要有两点:

  • NIO 是非阻塞的
  • NIO 面向块,I/O 面向流

NIO 入门

NIO 总结:

  • 避免多线程
  • 单线程处理多任务
  • 非阻塞I/O (I/O读写不再阻塞,而是返回 0)
  • 基于block(块)的传输,通常比基于流的传输更高效
  • I/O 多路复用大大提高了Java网络应用的可伸缩性和实用性

NIO与传统IO的区别

你可能感兴趣的:(Java)