Java NIO学习小结

前面一篇主要学习了下IO的流式操作,接下来就是重头戏了,NIO,又称为New IO

当然也是得抱着问题来学习这个东西了,希望可以通过本文,可以学习到:

  • 什么是NIO
  • NIO相比较与IO有什么特点
  • 同步,非同步,阻塞,非阻塞是什么鬼
  • 几种IO模型

I. 基本概念

首先理解下什么是同步IO,非同步IO,什么是阻塞IO,非阻塞IO,它们两对的主要区别是什么;其次就是五种IO模型

1. 同步/非同步IO

a. 同步IO

同步,主要是多个线程的执行中,对彼此的执行结果有依赖,即某个线程的执行,必须要求他依赖的线程二执行完毕

同步IO,表示在发起IO操作之后,如果数据没有准备就绪,就需要用户线程轮询的去询问是否准备好,只有准备好之后,将数据从内核拷贝到用户线程

b. 异步IO

异步,指多个任务可以并发的执行,他们比吃执行结果,是否执行完毕对其他都没有影响

异步IO,用户线程发起IO操作,该用户线程可以继续执行其他的事情;剩下的数据是否准备完毕,准备完毕之后从内核拷贝到用户都有内核自动完成

c. 区别

同步IO和异步IO的主要区别就在于:

  • 用户线程发起IO操作之后,是否可以干其他的事情(同步IO需要轮询判断数据是否准备就绪;异步IO不需关心)
  • 数据从内核拷贝到用户线程
  • 同步IO会阻塞用户线程;异步IO不会

2. 阻塞/非阻塞IO

a. 阻塞IO

阻塞,指在执行过程中,没有获得预期的结果,就一直阻塞等待获取到结果

阻塞IO,在发起IO请求之后,若数据没有准备好,就一直阻塞等待数据准备完毕

b. 非阻塞IO

非阻塞,表示在执行过程中,若某个条件未满足,则直接返回个标识,它继续去干其他的事情

非阻塞IO,在发起IO请求之后,若数据没有准备好,就返回一个对应标识,它继续干其他的事情

3. 五种IO模型

a. 阻塞IO模型

最传统的IO模型,在读写数据时,未准备就绪,则阻塞用户线程,释放CPU资源,当数据准备就绪之后,内核将数据拷贝到用户线程,用户线程取消阻塞状态

b. 非阻塞IO模型

在读写数据时,直接返回结果,如果没有准备好,则自己实现逻辑,轮询的去判断是否准备就绪

当准备完毕之后,再次轮组发起IO请求,就可以将数据拷贝到用户线程

这个过程中,虽然没有释放CPU资源,但是轮询的判断是非常消耗性能的

c. 多路复用IO模型

Java NIO实际上就是多路复用IO。

在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用

在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。

d. 信号驱动IO模型

在发起IO请求时,注册一个信号驱动钩子,然后自己干自己的事情

当数据准备就绪之后,发送一个信号给用户线程,然后用户线程执行自己注册的钩子,在内部实现真实的IO操作

e. 异步IO模型

异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了

f. 说明

  • 前面四种都属于同步IO(在内核进行数据拷贝都会引起用户线程阻塞),只有最后一个是异步IO
  • 异步IO和信号驱动IO的主要区别在于具体的数据处理上

II. NIO

为了解决传统IO的阻塞问题引入的,主要原理如下:

  • 一个专门的线程来处理所有的 IO 事件,并负责分发
  • 事件驱动机制
  • 线程通讯通过 wait/notify 等方式通讯,较少线程切换

1. 基础知识

NIO新定义了三个基本角色:Channel, Buffer, Selector

a. Channel

类似IO中的流,但又有不同

  • 支持读写(而流是单向的)
  • 与Buffer进行交互(即写入到buffer,从buffer中读取)
  • 支持异步

常见的Channel有四种

  • FileChannel : 文件,不支持非阻塞方式
  • DatagramChannel:UDP网络数据
  • SocketChannel:TCP读写网络数据
  • ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

b. Buffer

缓冲区,主要有8种:

  • ByteBuffer
  • CharBuffer
  • FloatBuffer
  • DoubleBuffer
  • IntBuffer
  • ShortBuffer
  • LongBuffer
  • MappedByteBuffer

管道的读写是需要借助Buffer来实现的,一般buffer的读写流程:

  • 创建Buffer
    • ByteBuffer buf = ByteBuffer.allocate(48);
  • 写入数据到Buffer
    • 从Channel写到Buffer: channel.read(buf);
    • 直接塞入buffer: buf.put(12);
  • flip() 切换读写模式
    • 将写模式切换到读模式
  • 从Buffer读取数据
    • 从Buffer读数据到Channel:channel.write(buf);
    • 直接读取数据: buf.get()
  • 清空缓存区:clear() 或 compact()
    • clear 清空,但是数据并未清除,会覆盖
    • compact 将所有未读的数据拷贝到Buffer起始处

额外方法:

  • Buffer.rewind()
    • 将position设回0,所以你可以重读Buffer中的所有数据
    • limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)
  • Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用
  • Buffer.reset()方法,恢复到Buffer.mark()标记时的position

c. selector

是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件

  • 创建
    • Selector selector = Selector.open();
  • 注册通道
    • 设置通道为非阻塞 channel.configureBlocking(false);
    • 注册: SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
  • 监听事件
    • selector.select();当注册的事件到达时,方法返回;否则,该方法会一直阻塞
    • 迭代:selector.selectedKeys().iterator()
    • 获取通道: SelectionKey#channel
    • 通过缓冲区读写数据

III. NIO与IO对比

1. Java NIO提供了与标准IO不同的IO工作方式:

  • Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  • Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
  • Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

2. 使用场景

NIO

  • 优势在于一个线程管理多个通道;但是数据的处理将会变得复杂;
  • 如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,采用这种;

传统IO

  • 适用于一个线程管理一个通道的情况;因为其中的流数据的读取是阻塞的
  • 如果需要管理同时打开不太多的连接,这些连接会发送大量的数据;

3. 区别

  • IO是面向流的,NIO是面向缓冲区的
    • Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方;
    • NIO则能前后移动流中的数据,因为是面向缓冲区的
  • IO流是阻塞的,NIO流是不阻塞的
    • Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
    • Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取
    • 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
  • 选择器
    • Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道
    • 这些通道里已经有可以处理的输入,或者选择已准备写入的通道
    • 这种选择机制,使得一个单独的线程很容易来管理多个通道。

IV. 其他

参考

个人博客: Z+|blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

扫描关注

QrCode