一台 Java 应用服务器,排查时用 ps -eLf | wc -l 一数,线程数居然上万了,随即出现响应变慢、CPU 上下跳动、内存占用居高不下。很多 Java 应用跑着跑着线程就"膨胀"了:线程池配置不合理、任务里又嵌套提交任务、外部依赖超时导致线程堆积,最后线程数失控把机器拖垮。
线程不是越多越好。每个线程要占内存(默认栈大小在 1MB 量级,经验值)、要做上下文切换、要抢锁和 CPU。线程数超过合理范围后,性能不升反降,这就是"线程过多"的典型表现。这篇文章从排查入手,讲清楚 Java 线程数异常的常见根因、用哪些工具定位、以及线程池应该怎么配才合理。
适合读者:做 Java 后端运维或开发、遇到过线程暴涨、或者想系统理解线程池配置的人。下面先讲怎么快速看清线程全貌,再讲根因分类,最后落到线程池参数设计的实践。
一、先量出来:Java 线程数怎么看
排查第一步是确认线程到底有多少、都是些什么线程。
# 看进程的总线程数(LWP 数)
ps -o nlwp,pid,comm -p
# NLWP 就是线程数(number of light weight processes)
# 统计系统里所有 Java 进程的线程总数
ps -eLf | grep java | wc -l
# 单进程线程上限(max threads)受几方面限制,先看进程级限制
cat /proc//limits | grep -i "processes"
# 线程本质是轻量级进程,受 nproc 和 ulimit -u 限制
ulimit -u
进到 JVM 内部看线程详情,最直接的是 jstack:
# jstack 打印所有线程栈(需要和目标进程相同的 JDK)
jstack > /tmp/threaddump.txt
# 统计各状态线程数量
grep -c "java.lang.Thread.State" /tmp/threaddump.txt
# 按状态分组统计
grep "java.lang.Thread.State" /tmp/threaddump.txt | sort | uniq -c
# 输出示例:
# 320 java.lang.Thread.State: RUNNABLE
# 1200 java.lang.Thread.State: WAITING (parking)
# 850 java.lang.Thread.State: TIMED_WAITING (sleeping)
# 40 java.lang.Thread.State: BLOCKED (on object monitor)
jstack 的输出里,每个线程有名字("pool-1-thread-1" 这种)、状态、调用栈。线程名是关键线索:名字里带 pool-N-thread-M 说明来自某个线程池,数一下每个池的规模就能看出是哪个池在膨胀。
除了 jstack,还有更友好的工具:
# jcmd 可以一次性拿到线程数统计
jcmd Thread.print | grep -c "^\"" # 统计线程数量
jcmd Thread.print > /tmp/dump.txt
# 用 jstat 看线程相关(GC 部分)不是线程,但能辅助判断 JVM 压力
jstat -gcutil 1000 5
二、线程状态与它揭示的问题
Java 线程状态分六种,排查时重点关注这几种:
| 状态 | 含义 | 数量多说明什么 |
|---|---|---|
| RUNNABLE | 正在运行或就绪 | 正常,但要警惕是否在忙等 |
| BLOCKED | 等待进入 synchronized 临界区 | 锁竞争严重,被其他线程占着锁 |
| WAITING | 无限期等待(如 wait()、park()) |
常在等锁、等任务、等条件 |
| TIMED_WAITING | 有超时的等待(如 sleep、wait(n)) |
常见于线程池空闲线程、定时任务 |
| NEW | 刚创建未启动 | 通常短暂 |
| TERMINATED | 已结束 | 通常短暂 |
一个健康的应用,绝大多数线程应该是 TIMED_WAITING(线程池里空闲待命的线程)或 WAITING(等待任务)。如果出现大量 BLOCKED,说明锁竞争严重;如果出现大量 RUNNABLE 且 CPU 很高,说明在做计算或有忙等。
需要特别注意的是:RUNNABLE 不等于占用 CPU。一个线程处于 RUNNABLE 可能是在等网络 IO(Java 的阻塞式 socket 读在 JVM 看来也是 RUNNABLE),也可能是在真正执行。要区分得结合 top -H 和线程栈。
# 找出占 CPU 最高的线程(-H 显示线程)
top -Hp
# 记下高 CPU 的线程 ID(TID)
# 把十进制 TID 转成十六进制,去 jstack 里找对应 nid
printf "%x\n"
# 例如 TID 12345 转成 3039,在 jstack 里找 nid=0x3039 的行
grep -A 20 "nid=0x3039" /tmp/threaddump.txt
这套"top -H + printf + jstack"的方法,是定位"哪个线程在烧 CPU"的标准动作。
三、线程数过多的常见根因
根因一:线程池核心参数配得离谱。
比如把核心线程数设成几千、队列容量设成 Integer.MAX_VALUE、或者用了 Executors.newCachedThreadPool()。缓存线程池在任务堆积时会无限创建线程,是最容易导致线程暴涨的写法。生产环境建议手动 new ThreadPoolExecutor,明确核心数、最大数、队列和拒绝策略。
根因二:任务里嵌套提交任务,形成死锁式堆积。
一个线程池里的任务又往同一个池提交子任务并等待结果。如果池被父任务占满,子任务永远排不上,父任务一直等,线程只增不减。这类问题的特征是有任务在 WAITING 且栈里能看到 Future.get()、CountDownLatch.await()。
根因三:外部依赖没有超时,线程被长时间占住。
调用下游 HTTP、数据库、第三方接口时没设 timeout,下游一慢,线程就无限等,池很快被占满,新请求只能排队或创建新线程,线程数飙升。这是生产里最常见的"线程雪崩"来源。所有外部调用都必须显式设置连接超时和读超时。
根因四:定时任务重复创建线程。
用了 new Thread()、new Timer() 却没复用,或者定时器每次都新建线程池。长跑之后线程越堆越多。
根因五:线程泄漏(ThreadLocal 或未关闭的资源)。
自定义线程或线程池没被正确 shutdown,或者 ThreadLocal 里的对象引用了大对象不放,都会导致线程和内存无法回收。
| 根因 | 典型特征 | 定位手段 | 修复方向 |
|---|---|---|---|
| 池参数过大/缓存池 | 线程多且多在空闲 | 看线程名统计池规模 | 改用手工线程池,限核心数 |
| 任务嵌套提交 | 线程多且多 WAITING | 栈里有 Future.get/await | 拆分线程池,避免同池嵌套 |
| 外部依赖无超时 | 某类线程长时间阻塞 | 栈里停在 socket 读 | 加超时、熔断、隔离 |
| 定时任务重复创建 | 线程名带 Timer 且持续增长 | 观察线程数随时间增长 | 复用线程池 |
| 线程泄漏 | 线程数只增不减 | 多次 dump 对比线程数 | 修 close/shutdown 逻辑 |
四、线程池参数怎么配才合理
这是最常被问的问题。给一套可操作的方法:
// 推荐的显式线程池写法(Java 8+)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8, // corePoolSize 核心线程数
16, // maximumPoolSize 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(200), // 有界队列,务必设上界
new ThreadFactory() { // 自定义线程名,便于排查
private final AtomicInteger n = new AtomicInteger(1);
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "biz-pool-" + n.getAndIncrement());
t.setDaemon(false);
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程执行,形成背压
);
核心线程数怎么估:
- CPU 密集型(计算、序列化、加解密):一般是 CPU 核数加 1 到 2。核数多、任务多的机器取
nproc或nproc + 1。 - IO 密集型(调数据库、调接口、读写文件):可以用"核数 × 目标利用率 ×(1 + 等待时间/计算时间)"来估算。比如 8 核、等待占比 90%、计算占 10%,则大约是
8 × 0.9 × (1 + 9)级别,但实际不要一次拉满,取几十的量级,配合压测微调。 - 混合型:拆成不同的池,别用一个池干所有事。
队列一定要有界。无界队列会让任务无限堆积、内存暴涨,而且永远触发不到拒绝策略,掩盖了真实的过载信号。有界队列加 CallerRunsPolicy 能把压力反压到上游,是更健康的做法。
五、排查与治理的实操流程
# 步骤1:确认线程总数和增长趋势
ps -o nlwp,pid,comm -p
# 隔几分钟再看一次,判断是稳定还是持续增长
# 步骤2:dump 线程栈并统计状态
jstack > /tmp/dump1.txt
grep "java.lang.Thread.State" /tmp/dump1.txt | sort | uniq -c
# 步骤3:按线程名统计各池规模
grep '^"' /tmp/dump1.txt | sed 's/".*//' | sort | uniq -c | sort -rn | head -20
# 能直观看到哪个池最大
# 步骤4:找高 CPU 线程
top -Hp
# 拿到 TID 转十六进制后回 jstack 定位
# 步骤5:间隔一段时间再 dump,对比线程数是否增长
jstack > /tmp/dump2.txt
grep -c "^\"" /tmp/dump1.txt
grep -c "^\"" /tmp/dump2.txt
# 步骤6:看是否有大量线程卡在同一个调用点
grep -A 10 "java.lang.Thread.State" /tmp/dump1.txt | grep "at " | sort | uniq -c | sort -rn | head
如果发现某类线程都卡在 java.net.SocketInputStream.socketRead0,基本可以确定是外部调用没超时;如果都卡在 Unsafe.park 且栈里能看到业务线程池,是池被占满;如果都卡在 Object.wait 且是 BLOCKED,是锁竞争。
治理动作分轻重缓急:能改代码就改(加超时、限线程池、拆池),改不动就先加监控和限流(限制入口 QPS、加熔断),临时止血可以调大池或重启。重启只治标,如果不改根因,过一段时间还会复发。
风险提示:调大 maximumPoolSize 或队列长度只是把爆发点往后推,会掩盖问题、消耗更多内存,不建议作为长期方案。生产上调整线程池参数通常需要改配置并重启应用,重启会中断服务,务必安排在维护窗口并确认能回滚。
六、监控与预防
把线程数纳入日常监控,比出事再查有效得多。关键指标:每秒新建线程数、活跃线程数、队列积压长度、拒绝任务数、线程池饱和度。
# 用 jstat 定期看 GC 与线程(辅助判断 JVM 压力)
jstat -gcutil 1000 3
# JMX 暴露线程池指标(Tomcat、自定义池都可暴露),再用 Prometheus 采集
# 关键 MBean 属性:activeCount、queueSize、completedTaskCount、largestPoolSize
预防要点:所有外部调用设超时、用有界队列、给线程命名、拒绝策略要明确、定期做线程数基线对比(正常情况下线程数应该是稳定的,一旦持续增长就是预警信号)。
常见问题
Q:Java 进程线程数多少算正常?
没有统一标准,取决于业务和线程池配置。一般来说,一个中等规模的 Web 应用几百到一两千线程是常见的。关键是看趋势:稳定就是正常,持续增长就有问题。
Q:jstack 报错 "Unable to open socket file" 怎么办?
通常是权限问题,需要和目标进程相同的用户身份执行,或加 sudo -u 。也可能是 /tmp/hsperfdata_* 目录权限异常,检查该目录所属用户。
Q:为什么加了线程池最大线程数,问题反而更严重?
因为线程不是免费的,线程多了上下文切换、内存、锁竞争都加剧。在 CPU 已经饱和或存在锁竞争的场景下,加线程只会让情况恶化。要先判断瓶颈是 CPU、IO 还是锁。
Q:怎么判断线程是在真算还是假等?
top -Hp 看线程 CPU 占用:占 CPU 的是真算,不占但状态是 RUNNABLE 的多半在等 IO。再结合线程栈看停在哪个调用点即可确认。
Q:用了 newCachedThreadPool() 怎么改?
改成显式的 ThreadPoolExecutor,设置合理的核心数、最大数和有界队列,配上命名线程工厂和像 CallerRunsPolicy 这样的拒绝策略。这是最值得优先整改的一类写法。
总结
Java 线程数过多的本质是"线程被无谓地创建或长时间占用"。排查顺序是:先量线程总数和趋势,再 dump 线程栈按状态分组,用线程名定位是哪个池,最后用 top -H + printf + jstack 找高 CPU 线程。
五大根因:线程池参数不合理(尤其缓存池)、任务嵌套提交、外部依赖无超时、定时任务重复创建、线程泄漏。其中"外部调用没设超时"是生产环境最常见的一条,务必优先整改。
线程池配置要点:CPU 密集取核数附近,IO 密集按等待比例估算并压测微调;队列必须有界;线程要命名;拒绝策略首选带背压的 CallerRunsPolicy。
治理要标本兼治:改代码(加超时、限池、拆池)治本,加监控和限流防复发,重启和调大参数只治标。
把活跃线程数、队列长度、拒绝数纳入监控基线,线程数"只增不减"就是最早的预警信号。
企业QQ咨询




