← 返回题目列表

Tomcat 的线程模型是什么?maxThreads、acceptCount 和 maxConnections 有什么区别?

高频 困难 第 14 / 23 题 更新于 2026/07/25
Tomcat线程模型maxThreadsNIO

简化版

Tomcat(NIO 模式)的线程模型分几类线程:Acceptor 线程负责接收新连接、Poller 线程用多路复用(Selector)监听已连接的 I/O 事件,就绪后交给 Worker 线程池(业务线程)执行 Servlet 逻辑。三个关键参数:maxConnections——Tomcat 能同时保持的最大连接数;acceptCount——连接数满了后,操作系统 TCP 等待队列的长度(排队等待被 accept 的连接数);maxThreads——Worker 线程池处理请求的最大线程数(决定同时能处理多少请求)。核心是「能保持多少连接」和「能同时处理多少请求」是两回事。

详细版

NIO 线程模型的三类线程:

新连接 → Acceptor(接收连接,1 个)
              ↓ 注册到
         Poller(Selector 多路复用,监听读写事件,几个)
              ↓ 事件就绪,取 Worker
         Worker 线程池(执行 Filter+Servlet 业务,maxThreads 个)
  • Acceptor:专门 accept() 新连接,接到后注册给 Poller。
  • Poller:持有 Selector,用少量线程监听大量连接的 I/O 事件(NIO 多路复用),有数据可读了才把请求交给 Worker。
  • Worker(线程池):真正执行 Filter 链 + Servlet 业务逻辑,数量由 maxThreads 控制。

三个核心参数对比:

参数含义默认值控制什么
maxConnections同时保持的最大连接数NIO 默认 8192能「接住」多少连接
acceptCount连接满后 OS 的等待队列长度100满了后能「排队」多少
maxThreadsWorker 线程池最大线程数200能同时「处理」多少请求

三者的关系(一个连接的命运):

  1. 连接数 < maxConnections → 被接收,进入处理。
  2. 连接数 = maxConnections → 新连接进 OS 的 acceptCount 等待队列排队。
  3. 队列也满了 → 新连接被拒绝(connection refused / 超时)。
  4. 被接收的连接,请求由 maxThreads 个 Worker 线程处理,Worker 都忙则请求等待。

完整版教学

一、为什么要理解线程模型:连接数 ≠ 处理能力

很多人以为「Tomcat 能扛多少并发」就看一个参数——错。「能保持多少连接」和「能同时处理多少请求」是两回事

  • 一个连接建立了,不代表它时刻在传输数据。HTTP keep-alive 下,连接可能大部分时间空闲,等着下一个请求。
  • Tomcat 用 NIO 多路复用少量 I/O 线程就能维持大量连接(连接空闲时不占用处理线程),只有真正有请求要处理时才占用 Worker 线程。

所以 Tomcat 参数分成两组:一组管连接(能接住/排队多少连接,maxConnections/acceptCount),一组管处理(能同时处理多少请求,maxThreads)。搞清这个区分,才能正确调参和排查。

二、NIO 线程模型的三类线程

以现代默认的 NIO Connector 为例:

① Acceptor 线程(接收连接): 专门循环 accept() 接收新 TCP 连接,接到后不自己处理,而是注册到 Poller。因为「建立连接」很快,Acceptor 数量很少(默认 1)。

② Poller 线程(监听 I/O 事件): NIO 的核心。Poller 持有一个 Selector,用多路复用同时监听成千上万个已建立连接的读写事件。某个连接有数据可读(客户端发来请求),Poller 才把这个就绪请求交给 Worker 线程池。连接空闲时不占用 Worker——这是 NIO 用少量线程扛海量连接的关键。

③ Worker 线程池(执行业务): 从这里取线程,执行完整的请求处理——Valve、Filter 链、Servlet 的 service()、业务逻辑(查库、调接口)。这才是真正「干活」的线程,数量由 maxThreads 决定。业务处理慢就占着 Worker 不放,Worker 用完新请求就得等。

线程/组件数量特征负责内容不负责内容
Acceptor通常很少accept() 新 TCP 连接执行业务代码
Poller少量Selector 监听大量连接读写事件长时间跑 Controller
Worker最多 maxThreadsValve、Filter、Servlet、业务逻辑维持所有空闲连接

记忆钩子:Acceptor 接客,Poller 看谁准备好点单,Worker 才真正做菜;连接多不代表厨房线程都在忙。

三、maxConnections:能接住多少连接

maxConnections = Tomcat 同时能保持的最大连接数

  • NIO 模式默认 8192(很大,因为 NIO 维持连接成本低)。
  • 达到 maxConnections 时,Tomcat 不再接收新连接(让它们去排队)。
  • 它反映「Tomcat 能同时握住多少个连接」。长连接、Keep-Alive、慢客户端都会占用连接名额,但这些连接很多处于空闲,不等于同时在执行业务的请求数

四、acceptCount:满了之后能排多长的队

acceptCount = 连接数达到 maxConnections、Tomcat 暂时接不下新连接时,操作系统 TCP 全连接队列的长度(对应 listen() 的 backlog)。

  • 默认 100
  • 新连接先在这个 OS 队列里排队,等 Tomcat 有空了再取出处理。
  • 队列也满了,操作系统拒绝新连接(客户端 connection refused 或超时)。

所以 acceptCount 是一个「缓冲队列」——短时流量突增时,多出来的连接先排队而非立即被拒,给 Tomcat 争取处理时间。它是缓冲,不是扩容能力:排太多队会让用户等待过久、超过客户端超时反而更糟,有时快速失败更好。

五、maxThreads:能同时处理多少请求

maxThreads = Worker 线程池的最大线程数,决定 Tomcat 同时能处理多少个请求

  • 默认 200
  • 每个正在处理的请求占用一个 Worker 线程,从进入业务逻辑到返回响应期间线程被占用。
  • maxThreads 用完,新的就绪请求就等待空闲线程。

maxThreads 调多大? 看业务是 CPU 密集还是 I/O 密集

  • CPU 密集(大量计算):线程数不宜过多(接近 CPU 核数),线程太多只增加上下文切换开销。
  • I/O 密集(大量等数据库、等远程调用,线程等待时不占 CPU):可设大一些(几百),因为线程大部分时间在等 I/O,多开线程能提高并发。大多数 Web 应用是 I/O 密集的,所以默认 200 比 CPU 核数大得多。

一个粗略估算公式:线程数 ≈ CPU 核数 × (1 + 平均等待时间/平均计算时间)——等待占比越高,可开的线程越多。最终仍要压测验证。

示例:8 核机器,平均每个请求 CPU 计算 20ms,等待数据库/HTTP 180ms
线程数估算 ≈ 8 × (1 + 180 / 20) = 8 × 10 = 80

如果等待时间变成 980ms:
线程数估算 ≈ 8 × (1 + 980 / 20) = 400

这只是帮助理解“等待占比越高,线程可适度更多”的方向,不是生产固定公式。真正配置还要看 JVM 内存、上下文切换、下游容量、压测曲线。线程开到 400,如果数据库只能稳定承受 100 个并发查询,Tomcat 侧再大也会把压力转嫁给数据库。

六、三个参数如何协同(一个连接的完整命运)

新连接到达
  ├─ 当前连接数 < maxConnections?
  │     是 → 被接收,Poller 监听它的请求
  │           └─ 请求就绪 → 有空闲 Worker(< maxThreads)?
  │                 是 → Worker 处理,返回响应
  │                 否 → 请求等待空闲 Worker
  │     否(连接已满)→ 进 OS 的 acceptCount 队列排队
  │                       ├─ 队列没满 → 排队等待被接收
  │                       └─ 队列也满 → 连接被拒绝

核心关系:maxConnections 管「能握住多少连接」,acceptCount 管「握不下时能排多长队」,maxThreads 管「能同时处理多少请求」——分别是连接容量、排队缓冲、处理能力

七、线程打满的真实原因与排查

Worker 线程打满(活跃线程数达到 maxThreads、新请求排队)时,盲目调大 maxThreads 往往是错的——真正的原因通常是业务处理慢,让线程被长时间占用:

常见原因:慢 SQL、远程服务超时、锁竞争、文件/网络阻塞、下游容量不足、请求量确实超过容量

正确排查步骤

  1. 活跃线程数、连接数、请求耗时分布、错误率——先量化现状。
  2. 抓线程栈(jstack),看大量线程卡在哪里——如果都停在数据库驱动、HTTP 客户端调用上,说明是下游慢拖住了线程。
  3. 对症下药:是下游慢就治理下游 + 设合理超时(避免线程无限期等待);是锁竞争就优化锁;是真的流量大才考虑扩容或适度加线程。

误区:只调大 maxThreads,会让更多线程堆在等数据库上,甚至打垮数据库、放大故障——线程池只是缓冲,解决不了下游瓶颈。

八、常见误区与追问

  • 误区:maxConnections 就是并发处理请求数。 它控制同时保持的连接数,真正同时执行业务请求的是 Worker,受 maxThreads 影响。
  • 误区:acceptCount 越大越好。 它只是连接满后的排队缓冲,队列太长会让用户等待更久,甚至超过客户端超时。
  • 追问:为什么 NIO 能少量线程维持大量连接? Poller 使用 Selector 监听 I/O 就绪事件,空闲连接不需要独占 Worker 线程。
  • 追问:什么时候调大 maxThreads 有意义? 请求主要在等 I/O、下游容量也能承受、压测显示 Worker 是瓶颈时,适度调大才有意义。
  • 误区:线程打满一定是 Tomcat 参数太小。 更常见的是慢 SQL、外部服务无超时、锁竞争或下游容量不足导致 Worker 长时间不释放。
  • 追问:Keep-Alive 多会造成什么现象? 连接数可能很高,但如果请求不活跃,Worker 不一定忙,需要结合连接数和活跃线程一起判断。

九、加强记忆

Tomcat(NIO)线程模型:Acceptor(接新连接)→ Poller(Selector 多路复用监听 I/O 事件,少量线程维持海量连接)→ Worker 线程池(执行 Filter+Servlet 业务,真正干活)。三个参数分管连接容量、排队缓冲、处理能力maxConnections(默认 8192,同时保持的最大连接数)、acceptCount(默认 100,连接满后 OS 等待队列长度,满了则拒绝,是缓冲非扩容)、maxThreads(默认 200,Worker 线程池大小,同时处理请求数,I/O 密集可调大、CPU 密集接近核数)。核心:「能接住多少连接」≠「能同时处理多少请求」线程打满先抓 jstack 查阻塞点(多半是慢 SQL/下游超时/锁),治理下游+设超时,而非盲目调大 maxThreads(会放大故障)