(本文大部分内容非原创,是自己整理复习的知识点。在最下面都会给上所有知识点的来源参考或出处,需要深入了解可以通过链接跳转)

概述

IO模型可以理解为用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。而在我们Java中支持了3种的IO网络模型,分别是BIO、NIO、AIO。

这三种模型呢可以理解为是Java语言对操作系统的各种IO模型(五大IO模型)的封装。程序员在使用这些API的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。而在我们学习这种三种IO的时候一定要先了解这几个概念:同步与异步,阻塞和非阻塞。

同步与异步

同步与异步可以理解为一种通信机制,指应用程序与内核的交互而言。

同步:同步可以理解为用户发起一个请求调用之后,在请求完成之前,调用不返回用户也不执行。

异步:异步可以理解为用户发起一个请求调用之后,在请求完成之前 ,用户可以继续执行;当调用完成之后会通知用户,或者调用用户的回调函数。

阻塞与非阻塞

阻塞和非阻塞可以理解为一种调用状态,指的是进程在访问数据的时候采用的方式。

阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。

非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

概念区分

同步/异步是从行为角度描述事物的,而阻塞和非阻塞描述的当前事物的状态(等待调用结果时的状态)。

BIO(Blocking I/O)

同步阻塞IO,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端链接请求之后为每一个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这是典型的一请求一应答通信模型。

java开发模型 javaio模型_非阻塞

但是线程是宝贵的资源,而且创建和销毁以及切换的成本都很高。所以如果使用这种一对一应答通信模型,哪怕这个链接不做任何事情的话也会造成不必要的线程开销。可以想到如果在客户端并发量大量增加后这种模型就会出现因为线程数量急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵尸,不能对外提供服务。

伪异步 IO

但是这种情况是可以改善的,为了解决同步阻塞IO面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

它的过程主要是:客户端接入的时候,将客户端的Socket封装成一个Task然后投递到后端的线程池进行处理,JDK维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。

java开发模型 javaio模型_数据_02

总结

伪异步IO通信框架采用了线程池实现,因此避免了为么一个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。

适用场景

适用于连接数量比较小且固定的架构,而且服务器资源多。这是JDK1.4以前的唯一选择,但是程序简单容易理解。

NIO

NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

同步是指线程还是要不断接收客户端连接并处理数据,非阻塞是指如果一个管道没有数据,不需要等待,可以轮询下一个管道。

NIO是支持面向缓冲的,基于通道的I/O操作方法。 它提供了与传统BIO模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。NIO的主要原理是服务器实现模式为多个连接请求对应一个线程,客户端连接请求会注册到一个多路复用器 Selector ,Selector 轮询到连接有 IO 请求时才启动一个线程处理。

java开发模型 javaio模型_数据_03

如果要详细一点介绍NIO的话,就可以从它的特性和三大核心组件开始介绍。

Buffer(缓冲区)

我们原来的IO是面向流(Stream),而NIO是面向Buffer(缓冲区)。Buffer是一个对象,它包含一些写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。

在NIO中,所有的数据读写操作都是通过这个缓冲区来完成的,而我们的缓冲区实质上就是一个数组。通常它是一个字节数组(ByteBuffer),同时也可以使用其他种类的数组。但它又不仅仅只是数组,它也提供了对数据的结构化访问以及维护读写位置等信息。

最常用的缓冲区是ByteBuffer,而Java基本类型除了Boolean都有对应的缓冲区,这里就不描述了。这些Buffer类型的类其实都是Buffer接口的一个子实例。它们都有完全一样的操作,只是处理的数据类型不一样而已。

Buffer的读写主要属性方法

Buffer有三个比较重要的属性,用来操作过程读写过程的位置。

  1. position:下次读写数据的位置
  2. limit:本次读写的极限位置
  3. capacity:最大容量

在我们的读写过程主要也有这三个比较重要的方法。

  1. flip :将写转为读,底层实现原理把 position 置 0,并把 limit 设为当前的 position 值。
  2. clear :将读转为写模式(用于读完全部数据的情况,把 position 置 0,limit 设为 capacity)。
  3. compact:将读转为写模式(用于存在未读数据的情况,让 position 指向未读数据的下一个)。

Buffer的读写过程举例

① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。

java开发模型 javaio模型_Java基础_04

② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。

java开发模型 javaio模型_非阻塞_05

③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。

java开发模型 javaio模型_IO模型_06

④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。

java开发模型 javaio模型_IO模型_07

⑤ 最后需要调用 clear() 或者compact()方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。

java开发模型 javaio模型_Java基础_08

⑥ 因为通道方向和 Buffer 方向相反,所以读数据相当于向 Buffer 写,写数据相当于从 Buffer 读。

java开发模型 javaio模型_非阻塞_09

Channel(管道)

双向通道,替换了 BIO 中的 Stream 流,不能直接访问数据,要通过 Buffer 来读写数据,也可以和其他 Channel 交互。同时也因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在Unix网络编程模型(五大IO模型)中,底层操作系统的通道全是全双工的,同时支持读写操作。

管道主要实现了顶级接口Channel,然后下面有很多扩展了的Channel,一般通用的包括以下类型

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

Selector(多路复用器)

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

具体过程就是:

轮询检查多个 Channel 的状态,判断注册事件是否发生,即判断 Channel 是否处于可读或可写状态。使用前需要将 Channel 注册到 Selector,注册后会得到一个 SelectionKey,通过 SelectionKey 获取 Channel 和 Selector 相关信息。

java开发模型 javaio模型_IO模型_10

我们在上面说过NIO是同步非阻塞的,所以当我们的Channel上的IO事件还没到达时,不会进入阻塞状态一直等待,而是继续轮询其他Channel,找到IO事件已经到达的Channel来执行。

同时需要注意的是只有套接字Channel(TCP,UDP)才能配置为非阻塞,而FileChannel不能,因为文件设置成了非阻塞也不会提升效率,文件总是可读可写的(就绪状态)。我们的非阻塞可以去轮询是否可以有数据可以写,如果没有的话可以继续轮询而不必要去等待减少阻塞时间,所以比较适用于网络IO。但是磁盘IO里的文件总是可以读写的,所以非阻塞没有什么意思。

NIO适合应用场景

适用于连接数目多且连接比较长轻操作)的架构,比如聊天服务器、弹幕服务器、服务器间通讯等,编程比较复杂,JDK1.4开始支持。

AIO

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。

异步IO的服务器实现模式为一个有效请求对应一个线程,客户端的 IO 请求都是由操作系统先完成 IO 操作后再通知服务器应用来直接使用准备好的数据。这里的异步是指服务端线程接收到客户端管道后就交给底层处理IO通信,自己可以做其他事情,非阻塞是指客户端有数据才会处理,处理好再通知服务器。

AIO模型在netty中未使用,也没有得到广泛的运用,所以也不用过多介绍了。

AIO适合应用场景

AIO适用于连接数目多且连接比较长重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

参考资料

《Netty权威指南》

Java NIO浅析——《美团技术团队》

BIO,NIO,AIO总结——《JavaGuide》

Java IO——《cyc2018》

Java IO模型之BIO、NIO、AIO三大IO模型