如何分析线程转储中导致线程WAITING的原因及应用挂起问题
分析Tomcat线程耗尽导致服务挂起的问题
Hey,我来帮你一步步拆解这个问题——你的场景是典型的线程资源耗尽但CPU/内存无异常,核心是大量线程卡在WAITING状态没释放,先给你梳理分析线程转储的实操方法,再针对你的情况给出排查方向:
一、先搞懂你的症状指向什么
线程数打满但CPU仅10%、内存无变化,说明不是CPU密集计算或内存泄漏,而是线程都卡在某个等待资源的环节——比如等数据库连接、等外部服务响应,或者锁等待(不过WAITING一般是等待通知,不是阻塞锁)。
二、手把手教你分析线程转储
fastthread.io没检测出异常很正常,它只是做通用检查,还要结合你的业务场景深挖:
1. 先筛选关键线程组
Tomcat的线程主要分两类,先聚焦核心工作线程:
http-nio-8080-exec-*(或http-bio/http-apr,看你用的连接器):处理HTTP请求的核心线程,这些线程打满的话,新请求就接不进来- 应用自定义线程:你代码里创建的业务线程池线程
把所有http-nio-8080-exec-*这类线程挑出来,看它们的状态和调用栈。
2. 深挖WAITING线程的调用栈
WAITING状态的线程通常是调用了Object.wait()或LockSupport.park(),重点看调用栈的最顶部3-5个方法:
- 如果看到
org.apache.tomcat.dbcp.dbcp2.PoolingDataSource.getConnection这类方法,那大概率是数据库连接池耗尽——所有工作线程都在等数据库连接,没新连接可用就一直挂着 - 如果是
java.util.concurrent.LinkedBlockingQueue.take(),可能是你的业务线程池阻塞在取任务,或者Tomcat的工作队列满了(不过你是线程数到上限,优先看工作线程的调用栈) - 如果调用栈里有第三方API的类(比如HttpClient、Redis客户端),那很可能是外部服务响应极慢/超时,线程被挂起没释放
- 如果是
java.lang.Thread.sleep(),那是代码里主动sleep,但一般不会批量出现
3. 对比正常/异常的线程转储
你可以在服务正常时抓一次线程转储,和挂起前的转储对比:
- 看哪些线程的状态从RUNNABLE变成了WAITING
- 找所有WAITING线程的调用栈共性——比如都卡在同一个数据库查询、同一个外部API调用
4. 结合日志和监控辅助验证
- 查Tomcat的
catalina.out或localhost.log,有没有Could not get a connection from pool这类连接池报错,或者Connection timed out的外部调用错误 - 看Datadog里的数据库连接数趋势、外部API调用耗时,是不是在挂起前这些指标突然飙升
三、针对你的场景的具体排查方向
1. 检查Tomcat连接器配置
打开server.xml的Connector节点,核对这几个参数:
maxThreads:Tomcat7默认是200,如果你设得太小,高并发下会打满;如果设得合理但还是耗尽,那是线程泄漏connectionTimeout:默认20000ms,如果设置太长,线程会一直挂着等请求处理完acceptCount:等待队列长度,默认100,队列满了新连接会被拒绝,但你的情况是线程数到上限,优先看前两个参数
2. 排查数据库连接池泄漏
如果用了DBCP/C3P0连接池:
- 检查代码里有没有获取连接后没关闭的情况——比如try块里拿了连接,finally里没写
conn.close() - 看连接池的
maxTotal(最大连接数)是不是太小,比如设置成10,但业务高峰需要20个连接,导致线程都在等 - 查数据库的连接数监控,是不是Tomcat的连接数一直上涨,直到打满数据库的最大连接数
3. 排查外部依赖的阻塞
如果应用调用了外部API、Redis、MQ等:
- 检查这些依赖的超时时间设置——是不是没设超时,或者超时时间太长(比如5分钟),导致线程一直挂起等响应
- 有没有用同步调用且没做超时处理,一旦外部服务变慢,线程就被占住不能释放
4. 检查线程泄漏
有没有代码里创建了线程但没正确关闭:
- 比如自定义线程池没调用
shutdown(),导致线程一直存活 - 用了ThreadLocal但没在请求结束后清理,导致线程持有对象不能被回收,线程一直存活
四、快速验证的小技巧
- 临时调整Tomcat参数:把
maxThreads调大到400,看是不是复发时间变长——如果是,说明确实是线程被占住没释放,不是配置太小 - 开启连接池监控:在Tomcat里配置连接池的JMX监控,实时看连接的
activeCount(活跃连接数)、idleCount(空闲连接数),确认是不是连接池耗尽 - 给外部调用加超时:比如用HttpClient的话,设置
connectTimeout=5000和socketTimeout=10000,强制超时释放线程
内容的提问来源于stack exchange,提问作者black sensei
相关产品推荐
相关产品推荐

