为什么 Tomcat 默认最大线程数是 200,而不是 N+1?
结论先行:N+1 是CPU 密集型任务的公式(N 为核数,+1 用于兜底偶发缺页),而 Web 请求是IO 密集型:线程大部分时间在等数据库、等下游、等磁盘,CPU 并未跑满。Tomcat 的 200 是「经验默认值 + 历史沿革」,正确值应该用 Little 定律估算:并发线程数 ≈ QPS × 平均响应时间,再留余量,而不是套 CPU 公式。
1. N+1 的适用边界
CPU 密集型:线程数 ≈ 核数 + 1
└─ 计算任务,线程一直占着 CPU,多了只会增加上下文切换
IO 密集型:线程数 ≈ 核数 × (1 + 等待时间 / 计算时间)
└─ 线程大部分时间阻塞在 IO,需要更多线程来填满 CPU
一次典型的 Web 请求:解析参数(微秒级)→ 查库(毫秒级)→ 组装 JSON(微秒级)。CPU 占用可能只有 5%,剩下 95% 都在等 IO。此时把线程数限制在核数 +1,CPU 反而闲死。
2. 用 Little 定律估算
并发数 L = 到达率 λ × 平均停留时间 W
| 场景 | QPS | 平均 RT | 需要的并发线程 |
|---|---|---|---|
| 轻接口(纯缓存) | 5000 | 5ms | 25 |
| 普通查库 | 1000 | 50ms | 50 |
| 复杂查询/多下游 | 300 | 300ms | 90 |
| 慢接口 | 50 | 2s | 100 |
| 峰值叠加 | — | — | 上面结果 × 1.5~2 倍余量 |
200 这个值大致覆盖「几百 QPS、百毫秒级 RT」的常规业务,所以成了默认值。
3. 200 从哪来
| 版本 | 默认 maxThreads | 说明 |
|---|---|---|
| Tomcat 4/5 | 200 | 沿用至今 |
| Tomcat 8.5/9 | 200 | minSpareThreads=10、acceptCount=100 |
| Tomcat 10.1 | 200 | 支持虚拟线程后可突破 |
它是 20 多年前的硬件条件下定的经验值,一直没改。注意「默认」不等于「最优」:很多团队上线时从未改过这个值,直到压测发现线程打满、队列堆积才回头调。
4. 调大不一定更好
线程数 ↑
├─ 上下文切换开销 ↑(每次切换 ~1~5μs,且污染 CPU 缓存)
├─ 每个线程栈内存 ↑(默认 1MB/线程,200 → 400 就多 200MB 虚拟内存)
├─ 下游压力 ↑(数据库连接池、第三方接口被打爆)
└─ 排队延迟不一定下降(瓶颈可能在下游,不在 Tomcat)
如果瓶颈是数据库连接池(比如只有 20 个连接),Tomcat 开到 500 也只会让 480 个线程在等连接,吞吐不变、RT 变长。线程数要和下游容量匹配。
5. 配置与验证
server:
tomcat:
threads:
max: 300 # QPS×RT 估算后取整,再留 50% 余量
min-spare: 30
accept-count: 200 # 全连接队列,抗瞬时突刺
max-connections: 8192
connection-timeout: 8s
<!-- 独立部署时的等价写法 -->
<Connector port="8080" protocol="Http11NioProtocol"
maxThreads="300" minSpareThreads="30"
acceptCount="200" maxConnections="8192" />
验证方式:压测时观察 http-nio-8080-exec-* 线程的忙碌比例(jstack 采样或 Micrometer 的 tomcat.threads.busy),如果长期 80% 以上且 CPU 不高,说明线程不够;如果上下文切换(vmstat 的 cs 列)飙升而 QPS 不涨,说明线程过多。
6. 虚拟线程带来的变化
Java 21 + Tomcat 10.1 可以启用虚拟线程:
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerCustomizer() {
return protocolHandler ->
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
虚拟线程在阻塞时自动让出载体线程,线程数不再受「平台线程昂贵」的约束,maxThreads 的意义随之弱化。但下游容量限制依然存在:数据库连接池不会因为虚拟线程变多而变大。
7. 常见追问
追问一:那 200 到底合不合理? 作为默认值合理(覆盖大多数中小流量场景),作为生产配置必须重新算。它更像「不做配置时的安全垫」,不是性能最优解。
追问二:为什么不是越多越好? 线程切换、内存占用、下游压力都会随线程数上升。超过下游承载能力后,增加线程只会让 RT 更差。
追问三:acceptCount 和 maxThreads 有什么关系?
maxThreads 是「正在处理的并发数」,acceptCount 是「处理不过来时排队的连接数」。两者共同决定「突发流量时是排队还是拒绝」。
追问四:线程数调大后 QPS 没涨,说明什么? 说明瓶颈不在 Tomcat 线程,而在下游(DB、Redis、第三方)或锁竞争。此时应该扩容下游或优化 SQL,而不是继续加线程。
追问五:IO 密集型的公式里「等待/计算」怎么估?
用压测数据:RT 减去 CPU 时间(jstack 采样或 APM 的 CPU 占比)就是等待时间。粗略估算时直接用「QPS × RT」更简单可靠。
小结
N+1 回答的是「CPU 怎么跑满」,Tomcat 的 200 回答的是「IO 等待时线程怎么够用」,两者针对的任务类型不同。生产配置的正确姿势是:Little 定律算基线 → 压测验证 → 与下游容量对齐 → 留 50% 余量,而不是抄一个默认值。