在 Java 的 IO 体系中,NIO(New IO / Non-blocking IO)是一个绑不开的话题。它是 Java 1.4 引入的一套新的 IO API,也是 Netty、Tomcat 等高性能框架的底层基石。
一、NIO 的三大核心组件
NIO 有三个最重要的概念:Channel、Buffer、Selector。我们用最通俗的方式来理解它们。
1. Channel(通道)
Channel 可以理解为一条双向的水管。
在传统的 BIO 中,我们用 InputStream(只能读)和 OutputStream(只能写),就像两根单向的管子。而 Channel 是双向的,既能读也能写。
那么 Channel 两端连接的是什么呢? 这取决于 Channel 的类型:
SocketChannel连接的是客户端与服务器,这是最经典的网络通信场景。当你用浏览器访问网站,或者 App 请求后端接口时,底层就是通过SocketChannel进行通信。FileChannel连接的是程序与本地文件,用于读写文件,比如读取配置文件、写日志、处理大文件上传下载等场景。ServerSocketChannel比较特殊,它本身不传输数据,而是用来监听端口、接受连接。每当有客户端连进来,它就会生成一个新的SocketChannel来处理这个连接。你可以把它想象成"守在门口等人来"的角色。
实际开发中,SocketChannel 和 ServerSocketChannel 这对组合用得最多,基本上所有网络通信框架的底层都是基于它们实现的。
2. Buffer(缓冲区)
Buffer 可以理解为一个临时存放数据的容器,就像一个水桶。
Channel 不能直接读写数据,必须通过 Buffer 中转。就像你用水管浇花,需要先用桶接水,再把桶里的水倒出去。
读数据时,数据从 Channel 流入 Buffer,再由程序从 Buffer 中取出。写数据时,程序先把数据写入 Buffer,再由 Buffer 流向 Channel。
Buffer 有几个重要的属性:capacity 表示容量,也就是这个桶有多大;position 表示当前位置,即读写进行到哪里了;limit 表示限制,即最多能读写到哪个位置。
3. Selector(选择器)
Selector 可以理解为一个前台接待员,这是 NIO 最强大的地方。
传统 IO 中,一个线程只能处理一个连接。但有了 Selector,一个线程就可以监控多个 Channel。就像一个前台接待员可以同时盯着多个门口,哪个门口有客人来了就去处理哪个。
它的工作方式是这样的:首先把多个 Channel 注册到一个 Selector 上,然后 Selector 会持续监控这些 Channel。当某个 Channel 有事件发生(比如有数据可读)时,Selector 会通知程序,程序只需要处理那些"有事"的 Channel 即可。
Selector 本质上就是 Java 对操作系统 IO 多路复用机制的封装。 在不同操作系统上,它依赖不同的底层实现:Linux 上使用 epoll,macOS 和 BSD 上使用 kqueue,Windows 上使用 select 或 IOCP。
这三个组件的协作关系可以这样理解:Selector 负责监控多个 Channel,当某个 Channel 有事件发生时,数据通过 Buffer 进行中转,最终到达程序进行处理。
简单总结一下:Channel 是数据的传输通道,就像双向水管;Buffer 是数据的临时存储,就像水桶;Selector 是管理多个 Channel 的调度者。
二、服务器并发模型的演进
理解了 NIO 的核心组件后,我们再来看看服务器处理并发连接的演进历史。这个演进过程能帮助你理解为什么 NIO 和 Selector 如此重要。
阶段一:多线程模型
最早的做法非常简单粗暴:每来一个客户端连接,就创建一个新线程去处理,处理完后线程销毁。
这种模型的优点是简单直观、代码好写。但问题也很致命:线程创建和销毁的开销很大,每个线程默认占用 1MB 左右的栈空间。如果有 1 万个连接就需要 1 万个线程,系统资源会直接耗尽。
这种模型只适用于连接数很少的小应用,比如几十个连接的场景。
阶段二:线程池模型
为了解决频繁创建销毁线程的问题,人们引入了线程池。做法是提前创建好一批线程放在池子里,有连接来了就从池子里拿一个线程去处理,用完后归还到池子里复用。
这种模型避免了频繁创建销毁线程的开销,也可以通过控制池子大小来防止资源耗尽。
但问题仍然存在:线程数量有限,比如池子里有 200 个线程,第 201 个连接就得排队等待。更关键的是,如果某个连接处理很慢(比如在等待 IO),线程就被占用着,其他连接只能干等。本质上还是一个线程同一时间只能处理一个连接。
这种模型适用于中等并发量的场景,大概几百到几千个连接。
阶段三:Selector 模型
Selector 模型彻底改变了思路:用一个线程(或少量线程)通过 Selector 监控所有连接,哪个连接"有事"就处理哪个。
核心思想不再是"一个线程守一个连接",而是"一个线程管一堆连接,谁有事就处理谁"。
这种模型的优势非常明显:少量线程就能处理海量连接,线程不会被阻塞在某个空闲的连接上,资源利用率极高。它适用于高并发场景,可以轻松支撑几万、几十万甚至百万级别的连接。
三个阶段对比
| 模型 | 线程数 | 能撑多少连接 | 典型应用 |
|---|---|---|---|
| 多线程 | 连接数 = 线程数 | 几十个 | 学习demo、小工具 |
| 线程池 | 固定数量 | 几百~几千 | 传统Tomcat、早期Web服务 |
| Selector | 很少(1~几个) | 几万~百万 | Netty、Nginx、Redis、Node.js |
三、总结
NIO 的核心就是三大组件和一种思想。三大组件是 Channel(双向通道)、Buffer(数据缓冲)、Selector(多路复用)。一种思想是用少量线程管理大量连接,从"一对一"进化到"一对多"。
这就是为什么 Netty、Nginx、Redis、Node.js 等现代高性能框架和中间件都选择了 IO 多路复用模型。理解了 NIO,就理解了高并发网络编程的基石。
