Tomcat NIO 的工作原理(Acceptor, Poller, Worker 线程的作用)
Tomcat 的 NIO(Non-blocking I/O)模型是其能够处理高并发连接的核心机制。相比于传统的 BIO(Blocking I/O,即 JIoEndpoint,Tomcat 8.5+ 已移除),NIO 模型通过多路复用(Multiplexing)技术,用少量的线程就能管理大量的连接。
Tomcat NIO 的核心处理类是 NioEndpoint。整个处理流程主要由三个核心组件协作完成:Acceptor(接收器)、Poller(轮询器) 和 Worker(工作线程/Executor)。
我们可以用一个餐厅的例子来类比:
- Acceptor:门口的迎宾,只负责把客人(连接)领进门。
- Poller:大堂经理/服务员,不断巡视所有桌子,看哪桌客人准备点菜了(数据可读)。
- Worker:后厨厨师,负责真正的做菜(处理业务逻辑)。
1. 核心组件详解
A. Acceptor(接收线程)
- 数量:通常为 1-2 个线程(默认 1 个)。
- 职责:
- 它是 TCP 连接的入口。
- 它在一个
while(true)循环中,主要执行serverSocket.accept()。 - 注意:这一步是阻塞的,它会一直等到有新的 TCP 连接进来。
- 工作流程:
- 当新的连接建立(三次握手完成),
accept()返回一个SocketChannel。 - Acceptor 将这个 Channel 设置为非阻塞模式(Non-blocking)。
- 它将这个 Channel 封装成
NioChannel对象。 - 关键动作:Acceptor 将这个对象转交给 Poller 处理(通常是将事件放入 Poller 的事件队列中)。
- Acceptor 立刻回到循环顶部,继续等待下一个连接。
- 当新的连接建立(三次握手完成),
B. Poller(轮询线程)
- 数量:通常为
Math.min(2, Runtime.getRuntime().availableProcessors()),即最多 2 个。 - 职责:
- 持有 Java NIO 的核心组件 Selector(选择器)。
- 负责管理所有已连接但尚未断开的 Socket。
- 负责监听 Socket 上是否有数据到来(
OP_READ)或是否可写(OP_WRITE)。
- 工作流程:
- Poller 运行在一个
while(true)循环中,主要做两件事:- 处理事件队列:查看 Acceptor 是否塞给了它新的连接。如果有,将新连接注册到自己的
Selector上,并关注OP_READ事件。 - Select(轮询):执行
selector.select()。这是一个阻塞操作(通常设置超时时间),它会询问操作系统:“我管理的成千上万个连接中,有哪些已经收到了数据包?”
- 处理事件队列:查看 Acceptor 是否塞给了它新的连接。如果有,将新连接注册到自己的
- 一旦
select()返回,说明有一个或多个 Socket 准备好了(例如 HTTP 请求报文已经到达网卡缓冲区)。 - 关键动作:Poller 从 Selector 中取出这些 Key,生成一个任务对象(
SocketProcessor),然后将这个任务扔给 Worker 线程池。
- Poller 运行在一个
C. Worker(工作线程池 / Executor)
- 数量:这是一个线程池,默认核心线程数 10,最大线程数 200(可在
server.xml中配置maxThreads)。 - 职责:
- 执行真正的业务逻辑(Servlet/Spring MVC 等)。
- 读取 Socket 中的数据(此时数据已经准备好,读取速度极快),解析 HTTP 协议,执行 Filter 链,调用 Servlet,生成响应。
- 工作流程:
- 从线程池中取出一个空闲线程。
- 执行 Poller 递过来的
SocketProcessor任务。 - 读取数据:调用
socket.read()。因为 Poller 保证了只有数据到了才会叫 Worker,所以这里几乎不会阻塞在“等待数据网络传输”上。 - 业务处理:执行你的 Java 代码(查数据库、计算等)。注意:这里通常是阻塞的。
- 发送响应:处理完后,将响应数据写入 Socket。
2. 整体工作流程图解
plaintext
Client (Browser)
|
| [1. TCP Connect]
v
+----------------+
| Acceptor | <-- 线程数:1
| (ServerSocket) | <-- 作用:只负责建立连接,不做业务,极快
+-------+--------+
|
| [2. Pass SocketChannel (NioChannel)]
v
+-------+--------+ [3. Register OP_READ]
| Poller | <-- 线程数:1~2
| (Selector) | <-- 作用:持有 Selector,轮询所有连接的数据状态
+-------+--------+
|
| [4. Data Arrived! (OP_READ ready)]
| [5. Create SocketProcessor task]
v
+-------+--------+
| Worker Pool | <-- 线程数:200 (默认)
| (Executor) | <-- 作用:解析 HTTP,执行 Servlet,查库,响应
+----------------+
3. 为什么要这样设计?(NIO vs BIO)
传统 BIO 的问题
在 BIO 模式下,Acceptor 接收到连接后,必须立刻分配一个 Worker 线程全程陪同。
- 如果客户端连上了但迟迟不发数据(比如网络慢,或者 Keep-Alive 保持连接),这个 Worker 线程就会阻塞在
read()上,傻傻地等待。 - 后果:如果有 10000 个连接,就需要 10000 个线程。线程切换开销巨大,内存瞬间爆炸。
NIO 的优势
在 NIO 模式下,Poller 充当了中间人。
- Poller 一个人(一个线程)就可以看着 10000 个连接。
- 只有当某个连接真的有数据到了,Poller 才会去叫 Worker 线程。
- 结果:
- Worker 线程不再把时间浪费在“等待网络数据传输”上。
- Worker 线程一上来就是干活(读内存数据、跑业务)。
- Tomcat 可以用 200 个 Worker 线程支持 10000+ 的并发连接(只要这些连接不是同时都在发数据)。
4. 总结
- Acceptor:负责连接(TCP Handshake)。
- Poller:负责中断/事件(等待数据到达,Selector 多路复用)。
- Worker:负责计算/业务(解析 HTTP,执行 Servlet)。
这种分层解耦的设计,使得 Tomcat 能够在高并发场景下,极大程度地降低线程消耗,提升吞吐量。