一、Tomcat NIO线程模型核心组件(Tomcat 8+默认)
Tomcat从8.0版本开始默认使用NIO(非阻塞IO) 线程模型,这是理解连接、连接线程和工作线程关系的基础。整个模型由三个核心组件构成,各司其职:
1. 核心组件定义与职责
| 组件类型 | 线程名称 | 数量 | 核心职责 | 对应配置参数 |
|---|---|---|---|---|
| Acceptor线程 | http-nio-8080-Acceptor-0 |
1个/端口 | 监听TCP连接请求,建立Socket连接 | acceptCount(等待队列长度) |
| Poller线程 | http-nio-8080-ClientPoller-0 |
默认2个 | 监听已建立连接的IO事件(读/写) | pollerThreadCount |
| 工作线程(Executor) | http-nio-8080-exec-* |
核心10,最大200(默认) | 执行应用层业务逻辑,处理请求和响应 | threads.min-spare/threads.max |
2. 组件关系图(文字描述)
客户端
|
| TCP连接请求
v
Acceptor线程(1个):接受连接,创建SocketChannel
|
| 注册到Poller
v
Poller线程池(默认2个):多路复用监听所有Socket的IO事件
|
| 当有读事件时,将任务提交给
v
工作线程池(默认最大200个):执行Servlet.service()方法,处理业务逻辑
|
| 处理完成后,将写事件注册回Poller
v
Poller线程:监听写事件,准备发送响应
|
| 当Socket可写时,通知工作线程
v
工作线程:将响应数据写入Socket
|
v
客户端
二、关键概念澄清:连接 vs 连接线程 vs 工作线程
1. 连接(TCP连接)
- 本质:操作系统内核维护的一个Socket对象,代表客户端与服务器之间的一条通信链路
- 占用资源:操作系统文件描述符(FD)、少量内核内存
- 生命周期:从TCP三次握手完成开始,到四次挥手结束
- 数量限制 :由Tomcat的
server.tomcat.max-connections(默认8192)和操作系统的最大文件描述符限制共同决定
2. 连接线程(Acceptor+Poller)
- 本质:Tomcat内部用于管理连接的专用线程
- 特点:数量极少且固定,与并发连接数无关
- 工作方式:使用IO多路复用(Java NIO Selector)技术,一个Poller线程可以同时监听数千甚至数万个连接的IO事件
- 资源占用:每个线程占用约1MB栈内存,以及Selector对象的开销
3. 工作线程(Executor线程)
- 本质:执行应用代码的线程池
- 特点:数量可配置,是应用层并发能力的瓶颈
- 工作方式:从队列中获取任务(请求处理),执行完成后返回线程池
- 资源占用:每个线程占用约1MB栈内存,以及执行任务时的堆内存
4. 三者核心关系
- 一个连接 ≠ 一个线程:在NIO模型下,数千个连接只需要2个Poller线程管理
- 一个请求 ≠ 一个工作线程:在异步模型下,一个请求可以在多个工作线程上分段执行
- 工作线程 是稀缺资源:默认只有200个,而连接可以有8192个
- 连接线程 是充足资源:数量固定且极少,永远不会成为瓶颈
三、同步请求处理流程与资源变化
我们以一个典型的同步请求为例,详细说明每个阶段的资源占用情况:
阶段1:TCP连接建立
- 客户端:发送SYN包
- 服务器:Acceptor线程接受连接,创建SocketChannel
- 资源变化:占用1个连接名额(max-connections减1),占用1个文件描述符
- 线程占用:仅Acceptor线程(瞬间),无工作线程占用
阶段2:请求数据读取
- Poller线程:监听到Socket的读事件
- Poller线程:将"处理请求"任务提交给工作线程池
- 工作线程:从线程池获取一个空闲线程,开始读取请求数据
- 资源变化:占用1个工作线程
- 线程占用:1个工作线程
阶段3:业务逻辑执行
- 工作线程:调用DispatcherServlet,进入控制器方法
- 工作线程:执行耗时操作(如数据库查询、RPC调用)
- 资源变化:工作线程持续被占用,无法处理其他请求
- 线程占用:1个工作线程(阻塞中)
阶段4:响应数据写入
- 工作线程:生成响应数据,写入ServletResponse
- 工作线程:将写事件注册到Poller
- Poller线程:监听到Socket的写事件,通知工作线程
- 工作线程:将响应数据从ServletResponse写入Socket
- 资源变化:工作线程即将释放
- 线程占用:1个工作线程
阶段5:请求处理完成
- 工作线程:完成响应,调用Servlet.service()方法返回
- 工作线程:释放,回到线程池
- 资源变化:释放1个工作线程;连接可能保持(HTTP/1.1 keep-alive)或关闭
- 线程占用:无工作线程占用
同步模型资源占用总结
- 整个请求处理期间:始终占用1个连接名额
- 从请求读取到响应完成:始终占用1个工作线程
- 瓶颈:工作线程池大小(默认200),最多同时处理200个请求
四、DeferredResult异步请求处理流程与资源变化
现在我们来看使用DeferredResult时,各个阶段的资源变化情况,重点对比与同步模型的不同:
阶段1-2:TCP连接建立与请求读取
- 与同步模型完全相同
- 资源变化:占用1个连接名额,占用1个工作线程
- 线程占用:1个工作线程
阶段3:控制器方法执行与DeferredResult返回
- 工作线程:调用控制器方法
- 控制器:创建DeferredResult对象,将其保存到某个地方(如队列、Map)
- 控制器:立即返回DeferredResult对象
- Spring MVC :调用
request.startAsync(),启动异步上下文 - Spring MVC:保存DeferredResult和AsyncContext,注册监听器
- 工作线程:从控制器方法返回,DispatcherServlet处理完成
- 资源变化 :释放1个工作线程!连接仍然保持
- 线程占用 :无工作线程占用!这是异步模型的核心优势
阶段4:异步等待状态(最关键的阶段)
- 状态:DeferredResult尚未被设置结果
- 资源变化 :
- ✅ 连接仍然占用1个连接名额
- ✅ Poller线程仍然监听该连接的事件(如客户端断开)
- ❌ 不占用任何工作线程!
- ❌ 不占用任何连接线程(Poller只是多路复用监听)
- 线程占用 :零应用线程占用!
- 持续时间:可以是几毫秒到几十秒(取决于超时设置)
阶段5:业务逻辑执行与结果设置
- 任意线程:执行业务逻辑(可以是自定义线程池、消息队列消费者、定时任务等)
- 业务线程 :调用
deferredResult.setResult(result) - DeferredResult:触发内部回调,通知Spring MVC结果已准备好
- Spring MVC :将"渲染响应"任务重新提交到工作线程池
- 资源变化:占用1个工作线程(从线程池重新获取)
- 线程占用:1个工作线程(执行响应渲染)
阶段6-7:响应写入与处理完成
- 与同步模型的阶段4-5完全相同
- 工作线程:渲染视图,将响应数据写入ServletResponse
- 工作线程:将写事件注册到Poller,最终写入Socket
- 工作线程 :完成响应,调用
asyncContext.complete() - 资源变化:释放1个工作线程;连接可能保持或关闭
- 线程占用:无工作线程占用
五、同步 vs DeferredResult 资源变化对比表
| 处理阶段 | 同步模型 | DeferredResult异步模型 |
|---|---|---|
| 连接建立 | 占用1个连接 | 占用1个连接 |
| 请求读取 | 占用1个工作线程 | 占用1个工作线程 |
| 业务执行 | 持续占用1个工作线程 | 释放工作线程,零占用 |
| 结果等待 | 持续占用1个工作线程 | 零工作线程占用 |
| 响应渲染 | 占用1个工作线程 | 重新占用1个工作线程 |
| 响应发送 | 占用1个工作线程 | 占用1个工作线程 |
| 处理完成 | 释放工作线程和连接 | 释放工作线程,连接可能保持 |
| 最大并发请求数 | 约等于工作线程数(200) | 约等于最大连接数(8192) |
六、DeferredResult场景下的线程状态时序图
时间线:
0ms: 客户端发送请求
1ms: Acceptor接受连接,注册到Poller
2ms: Poller检测到读事件,提交任务到工作线程池
3ms: 工作线程T1开始处理请求
5ms: 控制器创建DeferredResult并返回
6ms: Spring启动异步上下文,工作线程T1释放回线程池
7ms: 【关键】进入异步等待状态,无工作线程占用
... 1000ms: 业务逻辑在自定义线程T2中执行
1007ms: T2调用deferredResult.setResult()
1008ms: Spring提交响应任务到工作线程池
1009ms: 工作线程T3(可能是不同的线程)开始渲染响应
1015ms: T3完成响应写入,释放回线程池
1016ms: 连接保持(keep-alive)或关闭
七、关键结论与常见误区澄清
1. 最核心的结论
- DeferredResult释放的是工作线程,而不是连接
- 在异步等待期间,连接仍然被占用,但不占用任何工作线程