当你使用 spring-boot-starter-web 启动一个 Spring Boot 应用时,你有没有想过,那个在控制台一闪而过的 "Tomcat started on port(s): 8080" 到底意味着什么?
Spring Boot 究竟是如何“凭空”启动一个 Web 服务器的?它又是如何处理成百上千的并发请求的?答案就藏在 内嵌式 Tomcat (Embedded Tomcat) 及其核心的 线程池 里。
1. Tomcat 是什么?它在 Spring Boot 里干什么?
简单来说,Tomcat 是一个 Web 服务器 和 Servlet 容器。
在 Spring Boot 诞生之前,我们开发 Web 应用(比如 JSP/Servlet)需要先在服务器上安装一个 Tomcat,然后把我们的项目(打成 .war 包)部署到 Tomcat 的 webapps 目录下去运行。
Spring Boot 彻底改变了这一点。
- 内嵌 (Embedded): Spring Boot 把 Tomcat “塞” 进了你的项目里(作为一个 jar 包依赖)。
- 启动: 当你运行 Spring Boot 的
main方法时,Spring Boot 会在内部自动配置并启动这个内嵌的 Tomcat。 - 职责: 它的核心职责就一个——处理 HTTP 请求。
它的工作流程是:
- 监听指定的端口(如 8080)。
- 接收来自客户端(浏览器、App等)的 HTTP 请求。
- 将请求交给 Spring Boot 的核心处理器
DispatcherServlet。 - 等待 Spring Boot 处理完业务逻辑。
- 接收 Spring Boot 返回的响应,并将其打包成 HTTP 响应,回传给客户端。
2. Tomcat 线程池:它和 Java 的 ThreadPoolExecutor 是一回事吗?
这是个非常关键的问题。答案是:不完全是。
Tomcat 的线程池并不是我们平时用 new ThreadPoolExecutor(...) 创建的那个标准 JUC 线程池,尽管它们都实现了 JUC 的 Executor 接口。
Tomcat 内部有一个定制的线程池实现(例如 org.apache.tomcat.util.threads.ThreadPoolExecutor),它针对 Web 服务的场景(大量、短时间的请求)做了专门优化。
核心区别:Tomcat 线程池的工作策略
标准 JUC 线程池(
java.util.concurrent):
- 请求来了,如果核心线程(corePoolSize) 没满,就创建新线程。
- 如果核心线程满了,就把请求放进阻塞队列(BlockingQueue) 。
- 如果阻塞队列也满了,就看最大线程(maximumPoolSize) 是否已满。
- 如果没满,就创建新线程(救急线程)来处理。
- 如果最大线程也满了,就执行拒绝策略。
它的策略是:优先用满核心线程 -> 其次填满队列 -> 最后才扩容到最大线程。
Tomcat 线程池(默认策略):
- 请求来了,看当前线程数是否达到最大线程(max-threads) 。
- 如果没有达到最大线程数,立即创建新线程来处理这个请求。
- 如果已经达到了最大线程数,就把请求放进任务队列(TaskQueue) 。
- 如果任务队列也满了,就拒绝请求(通常是返回“连接拒绝”或超时)。
它的策略是:优先扩容到最大线程 -> 其次才使用队列。
为什么 Tomcat 要这么设计?
对于 Web 服务器来说,响应延迟是致命的。标准 JUC 线程池的“先入队”策略会导致请求在队列中等待,增加了响应时间。
Tomcat 的策略更“激进”:它假设 Web 请求都是短平快的,宁愿(在资源允许内)多创建几个线程来立即处理请求,也不愿意让请求在队列里排队。
3. 核心参数配置与 "说法"
在 Spring Boot 的 application.properties 或 application.yml 中,我们可以通过 server.tomcat.threads 来设置这些核心参数。
# 1. 最大工作线程数 (默认 200)
server.tomcat.threads.max=200
# 2. 最小备用线程数 (默认 10)
server.tomcat.threads.min-spare=10
# 3. 最大连接排队数 (默认 100)
server.tomcat.accept-count=100
# 4. 最大连接数 (默认 8192)
server.tomcat.max-connections=8192
这些参数设置有什么“说法”吗?当然有!调优的本质就是理解它们。
(1) server.tomcat.threads.max (最大线程数)
-
这是什么? 这是 Tomcat 线程池的“天花板”,决定了你的应用同一时刻最多能处理多少个请求。默认值是 200。
-
有什么说法? 这是最核心、最需要调优的参数。
- 设置太小: 并发量一上来,线程池瞬间占满,新请求全部进入
accept-count队列排队,导致响应变慢,甚至超时。 - 设置太大: 线程本身会占用内存(JVM 栈内存,默认 1MB 左右)。2000 个线程就可能吃掉 2G 内存。同时,大量线程在 CPU 间上下文切换(Context Switching) 的开销会非常大,反而导致性能下降。
- 设置太小: 并发量一上来,线程池瞬间占满,新请求全部进入
-
调优建议:
- CPU 密集型应用(例如,大量计算、加密):线程数不宜过高,通常设为
CPU 核心数 * 2左右,因为 CPU 一直在忙,线程再多也得排队等 CPU。 - I/O 密集型应用(例如,大量访问数据库、调用外部 API):线程数可以设高一些(如 200、400 甚至更高)。因为线程在等待 I/O(等数据库返回数据)时,CPU 是空闲的,可以切换去处理其他线程的请求。
- CPU 密集型应用(例如,大量计算、加密):线程数不宜过高,通常设为
(2) server.tomcat.threads.min-spare (最小备用线程)
-
这是什么? 即使没有请求,Tomcat 也会保持这么多“空闲”线程“待命”。默认值是 10。
-
有什么说法?
- 这是 Tomcat 的“预热”线程。当突发流量("毛刺")进来时,这些备用线程可以立即顶上去,而不需要经历“创建新线程”的开销。
- 如果你的应用经常面临突发流量,可以适当调高此值(例如 20 或 50),用少量资源换取更好的瞬时响应能力。
(3) server.tomcat.accept-count (连接排队数)
-
这是什么? 当所有工作线程(
max-threads)都在忙时,新来的连接请求会进入这个队列排队。默认值是 100。 -
有什么说法?
- 这是“最后一道防线”。它是一个 TCP 层的 backlog 队列。
- 如果
max-threads满了,accept-count队列也满了,那么新来的请求会直接被拒绝(客户端会收到 "Connection refused")。 - 这个值是你应用处理能力的“缓冲垫”。如果你的应用能很快处理完请求(即
max-threads很快能释放出来),这个值小一点没关系。如果处理慢,你又不想立即拒绝请求,可以适当调大它,但这也会导致排队请求的平均响应时间变长。
(4) server.tomcat.max-connections (最大连接数)
-
这是什么? Tomcat 在任何时候允许接收和保持的总连接数。默认 8192。
-
有什么说法?
- 不要和
max-threads搞混。Connection (连接) ≠ Thread (线程) 。 - HTTP/1.1 默认有
Keep-Alive机制,一个连接处理完请求后不会马上关闭,而是会“保持存活”一段时间,等待下一个请求。 max-connections管理的就是这些“正在处理的连接” + “保持存活的连接”。- 这个值必须大于等于
max-threads。通常保持默认值 8192 就足够了,它主要用来防止(如 C10K)恶意连接耗尽服务器资源。
- 不要和
4. 总结
- Spring Boot 内嵌了 Tomcat 作为默认的 Web 服务器,负责处理 HTTP 请求。
- Tomcat 的线程池是为 Web 场景定制的,它不是标准的 JUC
ThreadPoolExecutor。 - Tomcat 的策略是优先创建线程到
max-threads,以保证低延迟;而标准 JUC 线程池是优先填满core-threads和queue。 - 调优的核心是
max-threads(处理能力)和accept-count(缓冲能力),需要根据你的业务是 CPU 密集型还是 I/O 密集型来进行调整。
最后的建议: 不要在没有监控的情况下盲目调优。先使用 Spring Boot Actuator 或 Prometheus 监控 tomcat_threads_busy(繁忙线程)、tomcat_threads_current(当前线程)和 tomcat_connections_current(当前连接),找到瓶颈后再进行调整。