文章目录
-
- 从基础到高层:一条架构线
- 一、核心关系与"资源"指什么
- 二、后端项目对线程与资源的管理(总览)
-
- [1. 谁在管理线程](#1. 谁在管理线程)
- [2. 请求与线程、资源的对应关系](#2. 请求与线程、资源的对应关系)
- [三、Tomcat 与 Spring 的线程模型交互(重点)](#三、Tomcat 与 Spring 的线程模型交互(重点))
-
- [1. Tomcat 的线程池(底层)](#1. Tomcat 的线程池(底层))
- [2. Spring 对线程的扩展(上层)](#2. Spring 对线程的扩展(上层))
-
- [Spring 会不会自己创建线程池?(重要)](#Spring 会不会自己创建线程池?(重要))
- [3. 资源在线程间的归属关系](#3. 资源在线程间的归属关系)
- [四、Tomcat 启动 Spring 应用的完整流程](#四、Tomcat 启动 Spring 应用的完整流程)
- 五、企业级配置要点(线程与资源管理)
-
- [1. 调优 Tomcat 底层线程池(application.properties)](#1. 调优 Tomcat 底层线程池(application.properties))
- [2. Spring 异步线程池配置(统一管理异步任务线程)](#2. Spring 异步线程池配置(统一管理异步任务线程))
- [3. 保证异步场景下请求上下文不丢失(资源在线程间的传递)](#3. 保证异步场景下请求上下文不丢失(资源在线程间的传递))
- [4. 连接池与线程数的匹配(资源与线程的配比)](#4. 连接池与线程数的匹配(资源与线程的配比))
从基础到高层:一条架构线
理解线程与资源管理,可以按五层递进:谁在跑请求 → 资源归谁管 → 谁创建/扩展线程 → 跨线程如何传上下文 → 如何配置与排查。
| 层 | 要回答的问题 |
|---|---|
| 运行环境与业务应用 | 谁接收请求、谁执行业务?Tomcat 是运行环境,Spring 是业务应用,请求先经 Tomcat 再经 Spring。 |
| 线程与资源对应 | 一个请求几条线程?资源归谁管?典型模型:一请求一 Tomcat 工作线程;资源分请求级/连接池/共享。 |
| 线程模型交互 | 线程谁创建?Spring 会不会起池?只有 Tomcat 有请求处理线程池;@Async 时若不配置则每任务一线程(非池),配置后才是线程池。 |
| 资源归属与传递 | Request 跟谁走?异步为何拿不到?请求级资源跟线程走(ThreadLocal);异步需 TaskDecorator 或手动传递并清理;连接池与 maxThreads 匹配。 |
| 配置与可观测 | 参数怎么设?过载怎么拒?上下文怎么不丢?Tomcat 与 Spring 异步池显式配置;TaskDecorator 统一传上下文;统一命名便于 Dump 排查。 |
下文依此展开:先澄清 Tomcat 与 Spring 的角色及「资源」含义,再说明谁管线程、线程模型与 @Async 是否用池,最后是启动流程、企业级配置与总结。
一、核心关系与"资源"指什么
Tomcat 是 Servlet 容器,负责监听端口、接收连接、把请求交给工作线程;Spring (Context + Web MVC)是应用框架,负责 Bean 管理、请求分发、业务逻辑。Tomcat 是运行环境,Spring 是业务应用,Spring 应用跑在 Tomcat 里。
1. HTTP 请求 →
2. Tomcat(接收、分配线程)→
3. Spring MVC(DispatcherServlet)→
4. Spring 容器(Bean)→
5. 业务逻辑 →
6. 响应
后端项目里的资源 大致分四类:请求级 (Request、Session 等,随请求生命周期);连接类 (DB/HTTP/Redis 连接池,按需获取与归还);缓存与共享 (需考虑并发安全);线程本身(Tomcat 工作线程、@Async 所用线程)。线程管理的核心是:谁创建线程、请求由哪条线程执行、资源如何获取与释放、请求结束后如何回收线程与上下文。
二、后端项目对线程与资源的管理(总览)
1. 谁在管理线程
| 层级 | 谁管理线程 | 管理什么 |
|---|---|---|
| Tomcat | 内置线程池(如 ThreadPoolExecutor) |
接收连接、把请求交给工作线程;maxThreads、minSpareThreads、acceptCount 等控制并发与排队。 |
| Spring | 请求处理不创建线程池,复用车载 Tomcat 线程 | Controller/Service 在 Tomcat 线程中执行;@Async 时若不配置则每任务新起一线程(默认 SimpleAsyncTaskExecutor,非池),若显式配置 Executor 则使用该线程池。 |
| 业务/中间件 | 连接池、消息消费、定时任务等 | 数据库连接池内部可能有线程、消息监听器常自带线程池、Quartz 等有调度线程池。 |
2. 请求与线程、资源的对应关系
- 一个请求 在典型场景下对应 一个 Tomcat 工作线程(从请求进入到响应返回)。
- 该线程在执行过程中可能:从 连接池 取连接、从 RequestContextHolder 取当前请求上下文、从 Session 取用户信息;执行完毕后,连接归还池、线程归还 Tomcat 线程池,请求级上下文随之失效。
- 若使用 @Async ,主请求线程可提前释放,异步任务在另一条线程 执行(未配置 Executor 时是每任务一线程,配置了线程池则在该池中执行);此时若要用 Request/Session 等,需显式传递或恢复上下文,否则会丢。
一句话:后端通过"一个请求绑定一个工作线程"的模型,在该线程内完成对请求级资源与连接类资源的访问与释放;线程池参数和异步策略决定了并发能力和资源占用上限。
三、Tomcat 与 Spring 的线程模型交互(重点)
本节具体说明:Tomcat 如何提供请求处理线程池,Spring 如何在其之上做扩展。
1. Tomcat 的线程池(底层)
Tomcat 启动时会创建 内置线程池 (如 org.apache.tomcat.util.threads.ThreadPoolExecutor),核心作用:
- 每个 HTTP 请求到来时,Tomcat 从线程池分配一个 工作线程 处理;
- 该线程会一路贯穿到 Spring 层,Spring 业务逻辑(Controller/Service)默认就在这根线程里执行;
- 请求结束后,线程归还线程池,不会销毁,实现线程复用。
Tomcat 线程池关键参数(可在 server.xml 或 application.properties 配置):
- maxThreads:最大工作线程数(默认 200),决定同时处理的最大请求数,也间接决定"最多有多少请求同时在用连接、缓存等资源"。
- minSpareThreads:核心/常驻空闲线程数(默认 10),保持一定数量线程待命,减少冷启动延迟。
- acceptCount :请求队列长度(默认 100),当工作线程全部占用时,新请求在此排队,排满则拒绝。
2. Spring 对线程的扩展(上层)
Spring 不会替换 Tomcat 的底层线程池,而是在其之上做扩展:
- 默认场景 :Spring 业务代码(Controller/Service)都运行在 Tomcat 工作线程 中,无额外线程切换;请求级资源(Request、Session、RequestContextHolder)自然绑定当前线程。
- 异步场景 (
@Async/DeferredResult/CompletableFuture):耗时逻辑会放到另一批线程 执行,Tomcat 线程可提前释放;此时需注意 线程上下文传递,否则异步线程内拿不到当前请求的 Request/Session。
Spring 会不会自己创建线程池?(重要)
结论先说清:
| 场景 | Spring 是否创建/使用线程池 | 说明 |
|---|---|---|
| HTTP 请求处理(Controller/Service 同步执行) | 否 | Spring 从不 为请求处理创建线程池,所有请求都在 Tomcat 工作线程 中执行,Spring 只是复用车载线程。 |
| @Async 且未配置 Executor | 不算"池" | Spring 默认使用 SimpleAsyncTaskExecutor :每个 @Async 任务新起一条线程 ,执行完即销毁,没有线程复用 ,本质是"每任务一线程"而非线程池。生产环境若大量使用 @Async 且不配置,会导致无界创建线程,有 OOM 风险。 |
@Async 且显式配置了 Executor (如 ThreadPoolTaskExecutor) |
是 | 此时异步任务提交到你配置的线程池 中执行,线程复用、数量可控。生产环境使用 @Async 时必须显式配置线程池 ,并指定 @Async("beanName")。 |
线程上下文传递 :Spring 通过 RequestContextHolder (底层 ThreadLocal)保存当前请求的 RequestAttributes,仅当前请求所在线程可访问;异步线程中需通过参数传递或使用 TaskDecorator 包装,否则拿不到 Request/Session。
3. 资源在线程间的归属关系
| 资源类型 | 与线程的关系 | 谁管理生命周期 |
|---|---|---|
| Request / Response / Session | 与处理该请求的线程绑定,通常一个请求一个线程 | Tomcat + Spring(请求结束即失效) |
| RequestContextHolder | ThreadLocal,仅当前线程可见 | Spring(请求结束需清理,否则线程复用时可能串线) |
| 数据库连接池 / 连接 | 线程从池中 get ,用毕 return,不"属于"某一线程 | 连接池(如 HikariCP、Druid) |
| 异步线程池中的任务 | 由 Spring 或自定义 Executor 调度到 worker 线程 | Spring / 自定义 Bean |
四、Tomcat 启动 Spring 应用的完整流程
- Tomcat 启动 → 加载
web.xml或 Spring Boot 自动配置的 DispatcherServlet(Spring MVC 的核心 Servlet)。 - Tomcat 初始化 DispatcherServlet → 触发 Spring 容器初始化 (
ApplicationContext)。 - Spring 容器 扫描包、创建并初始化所有 Bean(Controller / Service / Mapper 等);若配置了连接池、异步线程池等,也在此阶段初始化。
- 初始化完成后,Tomcat 监听端口 ,线程池就绪,等待请求。
- 请求到来 :Tomcat 从线程池取一条工作线程 → 调用 DispatcherServlet → Spring MVC 匹配 Controller → 执行业务逻辑(期间可能从连接池取连接、读 Request/Session)→ 返回响应 → 线程归还池、请求级资源失效。
五、企业级配置要点(线程与资源管理)
结合"后端对线程与资源的管理",实际开发中需要关注以下配置。
1. 调优 Tomcat 底层线程池(application.properties)
Tomcat 线程池决定了 同一时刻最多有多少个请求在执行,也决定了连接池、缓存等资源的最大并发使用量。
properties
# Tomcat 线程池配置
server.tomcat.max-threads=500 # 最大工作线程数(IO 密集型可设 200--500)
server.tomcat.min-spare-threads=50 # 核心空闲线程数
server.tomcat.accept-count=200 # 队列长度(满则拒绝)
server.tomcat.threads.max-idle-time=60000 # 线程空闲超时(毫秒)
- maxThreads 不宜远大于数据库连接池等资源的容量,否则会出现大量线程在等连接。
- acceptCount 过大时,排队请求占用内存且 RT 变长,需配合监控与限流使用。
2. Spring 异步线程池配置(统一管理异步任务线程)
使用 @Async 时,必须显式配置线程池:Spring 默认使用 SimpleAsyncTaskExecutor(每任务新起一线程,不复用),生产环境会导致无界创建线程、有 OOM 风险。显式配置后便于控制并发度、统一线程命名,方便排查问题。
java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("asyncThreadPool")
public Executor asyncThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程数
executor.setMaxPoolSize(50); // 最大线程数
executor.setQueueCapacity(100); // 队列长度
executor.setThreadNamePrefix("async-"); // 线程名前缀(便于 Dump 排查)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
// 使用时指定线程池
@Async("asyncThreadPool")
public void doAsyncTask() {
// 耗时业务逻辑
}
3. 保证异步场景下请求上下文不丢失(资源在线程间的传递)
Spring 的 RequestContextHolder 基于 ThreadLocal,只对 当前线程 有效;异步线程拿不到主请求的上下文,需显式传递。
方式一:调用方传入上下文,在异步方法内设置
在主线程先取 RequestAttributes,通过方法参数传入异步方法,在异步方法开头调用 RequestContextHolder.setRequestAttributes(attributes, true),任务结束后 resetRequestAttributes()。注意:若主请求已结束,Request/Response 可能已失效,仅适合"主请求未结束、异步仅做并行计算"等场景。
方式二:使用 TaskDecorator 统一传递(推荐)
java
@Bean("asyncThreadPool")
public Executor asyncThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// ... 其他参数 ...
executor.setTaskDecorator(runnable -> {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return () -> {
try {
if (attributes != null) {
RequestContextHolder.setRequestAttributes(attributes, true);
}
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
});
executor.initialize();
return executor;
}
所有通过该线程池提交的异步任务都会自动带上当前请求的上下文,任务结束后清理,避免线程复用时串线。
4. 连接池与线程数的匹配(资源与线程的配比)
- 数据库连接池大小 :建议与 Tomcat
maxThreads和业务特点匹配;一般可设为略小于或等于"实际并发请求数",避免连接数过大压垮数据库。 - 线程命名 :Tomcat、Spring 异步线程池、连接池内部线程(若有)建议统一命名规范,便于用 Thread Dump 区分请求线程、异步线程、连接池线程,快速做问题归因。