Java IO编程模型概览

Summary of Java IO-Programming Model

Posted by decaywood on 2016-02-24
- 错误校对

传统的 BIO 编程

网络编程的基本模型为C/S模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。

在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口:Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。

下面通过下图所示的通信模型图来熟悉下BIO的服务器端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的请求–应答通信模型。

SVG

该模型最大的问题就是缺乏弹性伸缩的能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统性能急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。在高性能的服务器应用领域,旺旺需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能、高并发接入的场景。

伪异步IO编程模型

为了改进一线程一连接模型,后来又衍生出了一种通过线程池或者消息队列实现一个或多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞IO,所以被称为“伪异步”,下面u我们就对伪异步代码进行分析,看看伪异步是否能够满足我们对高性能、高并发接入的诉求。

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

采用线程池和任务队列可以实现一种叫做伪异步的IO通信框架,它的模型图如下图所示。

SVG

当有新的客户端接入的时候,将客户端的Socket封装成一个Task(该Task实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源耗尽和宕机。

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

NIO编程模型

Java NIO中有一个叫做Selector的重要概念,它是Java NIO编程的基础。Selector也叫做多路复用器,多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector会不断轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,以便进行后续的IO操作。

一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄的限制。这也意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是一个非常巨大的进步。

使用NIO编程的优点可以总结如下:

  • 客户端发起的连接操作是异步的,可以通过在多路复用器注册的OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。
  • SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样IO通信线程就可以处理其他的链路,不需要同步等待这个链路可用。
  • 线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,他没有连接句柄数的限制(仅受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,且性能不会随着客户端的增加而线性下降,因此,它非常适合做高性能、高负载的网络服务器。

模型图如下所示:

SVG

不过,如果严格按照UNIX网络编程模型和JDK的实现进行区分,实际上它只能被称为非阻塞IO,不能叫异步非阻塞IO。

AIO编程模型

NIO2.0提供了与UNIX网络编程事件驱动IO对应的AIO,引入了新的异步通道概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供两种方式获取操作结果:

  • 通过java.util.concurrent.Future类来表示异步操作的结果
  • 在执行异步操作的时候传入一个java.nio.channels

JDK底层通过线程池ThreadPoolExecutor来执行回调通知,异步Socket Channel是被动执行对象,我们不需要像NIO编程那样创建一个独立的IO线程来处理读写操作

不同IO模型对比

  同步阻塞IO(BIO) 伪异步IO 非阻塞IO(NIO) 异步IO(AIO)
客户端个数 : IO线程 1 : 1 M : N(M可以大于N) M : 1(一个IO线程处理多个客户端连接) M : 0(不需要启动额外的IO线程,被动回调)
IO类型(阻塞) 阻塞IO 阻塞IO 非阻塞IO 非阻塞IO
IO类型(同步) 同步IO 同步IO 同步IO(IO多路复用) 异步IO
API使用难度 简单 简单 非常复杂 复杂
调试难度 简单 简单 复杂 复杂
可靠性 非常差
吞吐量

参考:《Netty权威指南》