JVM 线程 RUNNABLE 状态排查陷阱:load 高 CPU 低场景深度分析
2026/7/24 9:45:28 网站建设 项目流程

本文是线上问题实战录系列的第 4 篇
叙事框架:现象 → 排查过程 → 根因 → 修复 → 预防


问题现象

线上问题排查中,线程状态分析是最常用的手段之一。但 RUNNABLE 状态并不等同于"线程正在高效执行",这是一个普遍存在的认知误区。本文以一个真实案例展开:Java 订单服务 CPU 使用率仅 25%,p99 响应时间却从 30ms 飙升至 823ms。通过 top、jstack、perf 等工具的组合分析,揭示了隐藏在 RUNNABLE 状态之下的真实瓶颈——大量线程处于"可运行但因资源竞争尚未被调度执行"的状态。本文详细还原了从现象到根因的完整排查链路。

排查过程

第一步:top —— CPU 不高,但 load average 很高

登录生产服务器,执行top,结果让人困惑:

CPU 空闲 68%,但 load average 高达 8.9。load average 远高于 CPU 使用率——说明有很多线程处于可运行状态,但因为某种原因没有被调度执行,或者在等待其他资源。

第二步:top -Hp —— 大量线程在 R 状态

top-Hp17892

输出显示 42 个线程处于 R 状态(在 Linux 中对应 TASK_RUNNING),但每个线程的 CPU 使用率不到 1.2%:

42 个线程都在 R 状态,但几乎不消耗 CPU——这本身就很不正常。

第三步:jstack —— 全都在 SocketInputStream,状态全是 RUNNABLE

jstack17892>/tmp/jstack-17892.txtgrep-A25'http-nio-8080-exec-97'/tmp/jstack-17892.txt

157 个线程的状态都是RUNNABLE,没有一个 BLOCKED 或 WAITING 的。

更诡异的是——所有线程都卡在java.net.SocketInputStream.socketRead0(Native Method)。明明是在等网络 I/O,JVM 却告诉你是 RUNNABLE。

这就是第一个认知陷阱:Java 里线程做网络 I/O(Socket.read())时,状态依然是 RUNNABLE,不会变成 BLOCKED 或 WAITING。

这是 HotSpot JVM 的实现细节:SocketInputStream.read()底层调用的是 interruptible I/O 系统调用,在 JVM 的线程状态模型中,这种状态被映射为 RUNNABLE。也就是说,JVM 的 RUNNABLE 不等于在 CPU 上执行,它包括了在等待网络/磁盘 I/O 完成的情况。

第四步:perf top —— 从内核视角看真相

既然 jstack 给不了答案,切换到 OS 级别的分析工具:

sudoperftop-p17892

结果一目了然:

符号占比含义
tcp_recvmsg42.32%内核 TCP 接收数据
sock_read15.18%socket 读取

57.5% 的 CPU 时间花在内核态的网络收包上——线程根本不是在跑业务逻辑,而是在等远程 socket 响应。

第五步:strace —— 精准确认

sudostrace-f-p17892-etrace=network-c

78% 的系统调用时间花在recvfrom上——所有线程都在做 socket 读操作。

证据链闭合了:

  1. jstack 说线程在SocketInputStream.socketRead0→ 做网络 I/O
  2. perf top 说内核在tcp_recvmsg→ 等 TCP 数据
  3. strace 说系统调用在recvfrom→ 读 socket

根因分析

排查到这一步,问题变成了:为什么所有线程都堆积在等一个网络响应上?

查代码发现,昨天上线的新版本引入了一个功能——调用外部评分服务score-api来丰富订单数据。实现使用的是 Java 11 的HttpClient

// OrderServiceV1.javaprivatefinalHttpClientclient=HttpClient.newHttpClient();

问题出在HttpClient.newHttpClient()这个默认构造方法上。

Java 11 的HttpClient.newHttpClient()使用的是内置的SimpleAsyncHttpClient默认每个路由只有 1 个连接。当 100 个请求同时进来:

  • 只有 1 个请求能拿到连接,正常发送
  • 其余 99 个请求排队等这个连接释放
  • 外部评分服务响应慢(100~300ms),连接被长时间占用
  • 队列越来越长,响应越来越慢

更糟糕的是,请求超时设了 30 秒,没有熔断机制——外部服务慢的时候,线程就这样全部挂住。

第二个认知陷阱:很多人以为HttpClient.newHttpClient()是"轻量级"的,但它的默认实现没有连接池,在高并发场景下等于隐形的共享瓶颈

修复方案

修复方案很明确:

  1. 自定义线程池:为 HttpClient 配置独立的线程池
  2. 连接超时:设置 connectTimeout,快速拒绝不可达的服务
  3. 请求超时:从 30 秒缩短到 5 秒
// OrderServiceV2.javaThreadPoolExecutorexecutor=newThreadPoolExecutor(4,8,30,TimeUnit.SECONDS,newLinkedBlockingQueue<>(100));this.client=HttpClient.newBuilder().executor(executor).connectTimeout(Duration.ofSeconds(3)).build();

同时,对于外部服务调用增加熔断机制(使用 Resilience4j 或 Sentinel),在外部服务故障时快速失败,避免线程池被拖垮。

验证结果

修复后的效果立竿见影:

指标修复前修复后
Load Average8.91.2
CPU 使用率25%14%
CPU Idle68%84%
RUNNABLE 线程数15724
p99 响应时间823ms42ms

避坑建议

1. 理解 JVM 线程状态的真实含义

JVM 的线程状态和操作系统线程状态不是一一对应的:

JVM 状态对应 OS 状态典型场景
RUNNABLETASK_RUNNING 或 TASK_INTERRUPTIBLE执行 CPU 计算等待网络/磁盘 I/O
BLOCKEDTASK_RUNNING等待进入 synchronized 块
WAITINGTASK_RUNNING 或 TASK_INTERRUPTIBLEObject.wait()、LockSupport.park()

RUNNABLE 不意味着在跑代码,它只意味着 JVM 认为这个线程"还能继续工作"。

2. 诊断工具配合使用

场景首选工具补充工具
CPU 高top -Hpjstackarthas thread -n 3
CPU 不高但服务慢perf topstrace -casync-profiler
怀疑网络 I/Operf top(找 tcp_recvmsg)sar -n TCPss -s
线程阻塞jstackArthasthread -b

3. HttpClient 使用规范

  • 禁止使用HttpClient.newHttpClient()默认构造
  • 必须使用HttpClient.newBuilder().executor(executor)自定义线程池
  • 必须设置connectTimeout(建议 3s)
  • 请求timeout不要超过业务容忍时间(建议 5s)

4. 外部调用三件套

任何对外部服务的 HTTP 调用都必须有:

  1. 连接池— 避免连接成为共享瓶颈
  2. 超时控制— connectTimeout + readTimeout + requestTimeout
  3. 熔断降级— 外部服务故障时快速失败,保护自身

5. 代码审查 Checklist 补充

在高并发路径上引入外部网络调用时,代码审查 checklist 中增加:

  • 是否使用HttpClient.newBuilder()而不是newHttpClient()
  • 是否配置了连接池和超时
  • 外部服务故障是否有熔断/降级策略
  • 是否有 fallback 兜底逻辑

附:完整命令清单

进程与线程级 CPU 排查

top-b-n1|head-30# 进程 CPU 排行top-b-n1-Hp<pid>|head-40# 线程 CPU 排行cat/proc/<pid>/status|grep-E'^(Name|Pid|Threads|VmRSS|State)'# 进程状态概览

线程堆栈分析

jstack<pid>>/tmp/jstack-<pid>.txt# dump 线程堆栈grep'Thread.State'/tmp/jstack-<pid>.txt|sort|uniq-c|sort-rn# 线程状态统计grep-A30'http-nio-8080-exec-97'/tmp/jstack-<pid>.txt|head-35# 查看业务线程堆栈grep-c'socketRead0'/tmp/jstack-<pid>.txt# 统计 socketRead0 线程数cat/tmp/jstack-<pid>.txt|grep-B2'socketRead0'|grep'Thread.State'|sort|uniq-c# 统计特定方法状态

内核级性能分析

sudoperftop-p<pid>--stdio2>/dev/null# CPU 热点分析(内核态)perfstat-etcp:tcp_rcv_space_adjust,tcp:tcp_receive_collapsed-p<pid>--sleep3# 内核 tracepoint 统计

系统调用分析

sudostrace-f-p<pid>-etrace=network-c2>&1# 网络系统调用耗时统计

网络连接排查

lsof-p<pid>|grep-cESTABLISHED# 统计 ESTABLISHED 连接数ss-tnp|grep<pid>|awk'{print $4}'|sed's/.*://'|sort-n|uniq-c|sort-rn|head-5# 连接端口分布ss-tnp|grep<pid>|grepscore-api|head-3# 按服务名过滤连接

修复验证

jstack<pid>|grep'Thread.State'|sort|uniq-c|sort-rn# 修复后线程状态验证jstack<pid>|grep-csocketRead0# 修复后排队线程数curl-w'\n'-o/dev/null-s'http://localhost:8080/api/v1/orders/enriched/v2?orderId=test001'# 接口验证

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询