NIO优化实现原理
网络 I/O 模型优化
最开始的阻塞式 I/O,它在每一个连接创建时,都需要一个用户线程来处理,并且在 I/O 操作 没有就绪或结束时,线程会被挂起,进入阻塞等待状态,阻塞式 I/O 就成为了导致性能瓶颈的 根本原因。
首先是一次简单的TCP数据传输:

首先,应用程序通过系统调用 socket 创建一个套接字,它是系统分配给应用程序的一个文件描述符;
其次,应用程序会通过系统调用 bind,绑定地址和端口号,给套接字命名一个名称;
然后,系统会调用 listen 创建一个队列用于存放客户端进来的连接;
最后,应用服务会通过系统调用 accept 来监听客户端的连接请求。
1. 阻塞式 I/O
在整个 socket 通信工作流程中,socket 的默认状态是阻塞的。也就是说,当发出一个不能立 即完成的套接字调用时,其进程将被阻塞,被系统挂起,进入睡眠状态,一直等待相应的操作 响应。从上图中,我们可以发现,可能存在的阻塞主要包括以下三种。
connect阻塞:

accept阻塞:

read、write 阻塞:

2. 非阻塞式 I/O
当我们把以上操作设置为了非阻塞状态,我们需要设置一个线程对该操作进行轮询检查,这也是最传统的非阻塞 I/O 模型。

3. I/O 复用

4. 信号驱动式 I/O
信号驱动式 I/O 类似观察者模式,内核就是一个观察者,信号回调则是通知。用户进程发起一 个 I/O 请求操作,会通过系统调用 sigaction 函数,给对应的套接字注册一个信号回调,此时 不阻塞用户进程,进程会继续工作。当内核数据就绪时,内核就为该进程生成一个 SIGIO 信 号,通过信号回调通知进程进行相关 I/O 操作。
信号驱动式 I/O 相比于前三种 I/O 模式,实现了在等待数据就绪时,进程不被阻塞,主循环可以继续工作,所以性能更佳。
5. 异步 I/O
信号驱动式 I/O 虽然在等待数据就绪时,没有阻塞进程,但在被通知后进行的 I/O 操作还是 阻塞的,进程会等待数据从内核空间复制到用户空间中。而异步 I/O 则是实现了真正的非阻塞 I/O。
当用户进程发起一个 I/O 请求操作,系统会告知内核启动某个操作,并让内核在整个操作完成 后通知进程。这个操作包括等待数据就绪和数据从内核复制到用户空间。
linux不支持,比较少见。
在 NIO 服务端通信编程中,首先会创建一个 Channel,用于监听客户端连接;接着,创建多 路复用器 Selector,并将 Channel 注册到 Selector,程序会通过 Selector 来轮询注册在其 上的 Channel,当发现一个或多个 Channel 处于就绪状态时,返回就绪的监听事件,最后程 序匹配到监听事件,进行相关的 I/O 操作。

零拷贝
零拷贝是一种避免多次内存复制的技术,用来优化读写 I/O 操作。
在网络编程中,通常由 read、write 来完成一次 I/O 读写操作。每一次 I/O 读写操作都需要 完成四次内存拷贝,路径是 I/O 设备 -> 内核空间 -> 用户空间 -> 内核空间 -> 其它 I/O 设 备。
Linux 内核中的 mmap 函数可以代替 read、write 的 I/O 读写操作,实现用户空间和内核空间共享一个缓存数据。mmap 将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址,不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射 到物理内存地址。这种方式避免了内核空间与用户空间的数据交换。I/O 复用中的 epoll 函数 中就是使用了 mmap 减少了内存拷贝。
线程模型优化
NIO 是基于事件驱动模型 来实现的 I/O 操作。Reactor 模型是同步 I/O 事件处理的一种常见模型,其核心思想是将 I/O 事件注册到多路复用器上,一旦有 I/O 事件触发,多路复用器就会将事件分发到事件处理器 中,执行就绪的 I/O 事件操作。
该模型有以下三个主要组件:
事件接收器 Acceptor:主要负责接收请求连接;
事件分离器 Reactor:接收请求后,会将建立的连接注册到分离器中,依赖于循环监听多路 复用器 Selector,一旦监听到事件,就会将事件 dispatch 到事件处理器;
事件处理器 Handlers:事件处理器主要是完成相关的事件处理,比如读写 I/O 操作。
1. 单线程 Reactor 线程模型
最开始 NIO 是基于单线程实现的,所有的 I/O 操作都是在一个 NIO 线程上完成。由于 NIO 是非阻塞 I/O,理论上一个线程可以完成所有的 I/O 操作。
但 NIO 其实还不算真正地实现了非阻塞 I/O 操作,因为读写 I/O 操作时用户进程还是处于阻 塞状态,这种方式在高负载、高并发的场景下会存在性能瓶颈,一个 NIO 线程如果同时处理 上万连接的 I/O 操作,系统是无法支撑这种量级的请求的。
2. 多线程 Reactor 线程模型
为了解决这种单线程的 NIO 在高负载、高并发场景下的性能瓶颈,后来使用了线程池。
在 Tomcat 和 Netty 中都使用了一个 Acceptor 线程来监听连接请求事件,当连接成功之 后,会将建立的连接注册到多路复用器中,一旦监听到事件,将交给 Worker 线程池来负责处理。
3. 主从 Reactor 线程模型
现在主流通信框架中的 NIO 通信框架都是基于主从 Reactor 线程模型来实现的。在这个模型 中,Acceptor 不再是一个单独的 NIO 线程,而是一个线程池。Acceptor 接收到客户端的 TCP 连接请求,建立连接之后,后续的 I/O 操作将交给 Worker I/O 线程。

参考
《Java性能调优实战》