Java NIO和多路复用(I/O multiplexing)

Java NIO多路复用依赖于具体的操作系统实现。I/O多路复用的核心,是事件多路分离器(Event Demultiplexer)概念,其对应NIO中的Selector。

传统java.io包基于流模型实现,以同步、阻塞的方式进行交互, I/O性能往往成为应用的性能瓶颈。Java NIO通过Channel、Buffer、Selector几方面的抽象,使构建多路复用、同步非阻塞I/O成为可能。

JDK1.7对NIO做了进一步改进,引入基于事件回调机制的异步非阻塞IO,被称为AIO(Asynchronous IO),NIO与AIO本质区别在同步与异步。

多路分离器(Event Demultiplexer)概念

Linux对内存管理采用虚拟寻址方式,应用程序请求虚拟地址,虚拟地址被转换成物理地址送到CPU,CPU把物理地址放入总线,读取内存中的数据。

以32位操作系统为例,Linux将虚拟空间划分成两部分,一部分为内核空间,另一部分为用户空间。Linux操作系统将最高的1G空间留给内核(虚拟地址0xC0000000~0xFFFFFFFF),称为内核空间,将较低的3G内存(虚拟地址0x00000000到0xBFFFFFFF)分配给应用进程,称为用户空间。

以读取文件为例,Linux内核首先读取磁盘,把文件读入内核缓冲区,再从内核缓冲区,把数据拷贝到应用程序缓冲区(缓存I/O又被称作标准I/O,大多文件系统默认I/O 操作都是缓存)。

比缓存I/O更进一步的是直接I/O,这种模式下应用程序直接从内核缓冲区读取数据。因此,Linux应用程序在读取文件时,需要等待内核先读取数据,然后从内核把数据拷贝到应用程序的进程中。

Linux5种I/O模式

阻塞 I/O(blocking IO)

内核准备数据,数据从内核拷贝到进程,都是阻塞操作。

非阻塞 I/O(onblocking IO)

应用进程在内核读取数据期间,反复的询问数据是否准备好。

I/O 多路复用(IO multiplexing)

当应用进程通过select读取文件(socket),应用进程会被block,于此同时内核会"监视”所有通过select,发出的文件读取请求(socket),任何一个文件(socket)的数据准备完毕,select就返回,应用进程再调用read操作,从内核中把数据读入应用程序进程。

异步 I/O(asynchronous IO)

应用进程在发起读取操作之后,就可以去做其它的事。当内核收到一个asynchronous read之后,会立刻返回。所以不会对应用进程产生任何block,内核等待数据读取完成,把数据拷贝到应用进程的内存区,一切处理完毕,内核会给应用进程发送一个信号,告知read操作已完成。应用进程不用反复检查I/O状态,也无需主动拷贝数据。

信号驱动 I/O(signal driven IO)

signal driven I/O不常用。

Linux I/O多路复用

Linux的select、poll、epoll 都是I/O多路复用的具体的实现,三种实现仅仅是历史原因,相继为前个版本问题的修复和优化。epoll目前只有Linux支持,BSD为kqueue,国内安卓系统厂商将epoll剔除之后,部分p2p软件的开发受此影响。其中最著名的Ngnix高性能并发就得益于epoll。Ngnix的设计原则就是使用平台所支持的最高效I/O模型来实现自身功能。详细参见nginx连接处理方法

Java NIO多路复用

I/O多路复用简单的说,就是用单线程,记录并跟踪每个I/O流(sock)的状态,达到一个线程同时管理多个I/O流的效果 ,这就是Java I/O multiplexing。

I/O多路复用如同红娘,同时为多人牵线,犹豫的双方就先凉着,看对眼的就立即撮合成事。不再是一对一专属红娘。

基于Reactor模式的NIO

在事件分离器概念中Reactor模式为同步I/O,Reactor是一个被动事件分离和分发模型,将处理单元(handle)放入select(),等待事件就绪(读/写)。Reactor实现相对简单,对于任务耗时短暂的处理场景非常高效,可以解决C10K问题。如果在处理单元中引入固定数量的线程池,有助于提高效率。NIO Java代码示例

基于Proactor模式的AIO

Proactor模式为异步I/O。AIO基于Proactor模式,在发起写操作时,由操作系统进行异步处理,处理完成通知分离器,分离器回调处理单元。Proactor实现相对复杂,依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统较少,应用事件驱动的主流,还是通过select/epoll来实现,Proactor处理耗时较长的并发场景表现会很好。

Java NIO与AIO模型

Reactor模型

并行计算专家NIO设计者Doug Lea在Scalable IO In Java中,对Reactor作出了详细的说明。包括Reactor、Reactor多线程版本及Reactor的变种版本。

Reactor的模拟实现

从逻辑上模拟实现Reactor模式。

Main Reactor处理CONNECT事件(Acceptor),Sub Reactor负责调用Read、Send处理单元。在Read处理单元中,利用线程池以并发的方式处理decode、compute、encode等。Netty架构借鉴了此模型。

Reactor基础版

Reactor基础模型使用单线程顺序处理事件。

在Reactor中典型的事件类型包括连接、读取和写入,每种类型的事件都有一个处理单元(handler)。

Reactor模型中的多路分离器(JavaNIO中Selector的概念)统一接收请求,所有处理单元都注册到多路分离器(Selector)中,多路分离器(Selector)根据事件类型,通知相应的处理单元,处理单元遍历事件集合(可能同一时刻会发生多个同类事件),顺序对每个事件进行处理。

basic reactor design

Reactor多线程版本说明

Multithreaded Designs(多线程版本的设计)

策略性地添加线程以实现可伸缩性,主要适用于多处理器的场景。

Worker Threads

Reactors的处理单元应该快速完成响应。处理单元的处理速度,直接影响Reactor的速度,应将非IO操作转移到其他线程中。

Multiple Reactor Threads

Reactor线程可以使I/O完全饱和,将负载分配给其他Reactor,用“负载均衡“的思路去匹配CPU和I/O速率,转移非I/O操作以加快速度。

worker thread pools reactor

对Reactor的扩展使用线程池,能够方便调整和控制线程数量,线程池中的线程数量,通常需要比客户端数量少很多。

NIO服务器

用NIO做服务器不再受线程方面的约束,因为它只用一个线程,只受限于CPU的性能。使用此模式做服务端,处理由AJAX发出的轮询,或高延迟的异步请求。Undertow基于XNIO,XNIO能够与操作系统直接I/O进行集成,如果应用场景要求不是很苛刻,又不想自行开发,可以试试高性能Web服务器Undertow