本文目录导读:

线程转储(Thread Dump)是分析 Java 应用线程状态的重要工具,特别适合定位线程池阻塞问题,以下是系统性的分析方法:
获取线程转储的时机
关键时机点
- 应用响应变慢或卡死时:立即获取,获取间隔几秒的多次转储
- CPU 占用异常时:配合
top -H或jstack查看线程 CPU 使用 - 功能挂起时:在故障发生期间获取,而非恢复后
获取命令
# 方式1:jstack(推荐) jstack -l <pid> > threaddump_$(date +%Y%m%d_%H%M%S).txt # 方式2:kill -3(Linux) kill -3 <pid> # 输出到标准输出 # 方式3:Java VisualVM / Java Mission Control # 在 GUI 工具中导出线程转储 # 方式4:Docker 容器内 docker exec <container_id> jstack -l <pid> > dump.txt
线程转储核心结构解读
线程状态分类
"pool-1-thread-1" #1 prio=5 os_prio=0 cpu=1234.56ms elapsed=9876.54s tid=0x00007f1234 nid=0x1234 **WAITING** (parking)
at java.util.concurrent.locks.LockSupport.park(Native Method)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(...)
at java.util.concurrent.LinkedBlockingQueue.take(...)
at java.util.concurrent.ThreadPoolExecutor.getTask(...)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(...)
at java.lang.Thread.run(...)
关键状态含义
| 状态 | 说明 | 常见阻塞场景 |
|---|---|---|
| RUNNABLE | 正在执行,但可能因 IO/锁而实质阻塞 | 网络 IO、文件 IO、数据库查询慢 |
| BLOCKED | 等待锁资源 | 线程竞争同步块/方法 |
| WAITING | 无限期等待 | 无超时的 wait()、join()、park() |
| TIMED_WAITING | 有超时的等待 | sleep()、带超时的 wait()、poll() |
分析线程池阻塞的四大模式
模式1:工作线程全部 WAITING(线程池空闲/任务队列空)
"pool-1-thread-1" WAITING at LinkedBlockingQueue.take()
"pool-1-thread-2" WAITING at LinkedBlockingQueue.take()
"pool-1-thread-3" WAITING at LinkedBlockingQueue.take()
- 原因:线程池空闲,没有任务提交
- 处理:正常状态,无需处理
模式2:工作线程全部 WAITING 但任务队列满(任务堆积)
"pool-1-thread-1" RUNNABLE at MyService.slowMethod()
"pool-1-thread-2" RUNNABLE at DatabaseRepository.query()
"pool-1-thread-3" TIMED_WAITING at Thread.sleep(10000)
"main" WAITING at ArrayBlockingQueue.put() <- 提交任务阻塞
- 原因:任务执行太慢,队列满,新任务无法提交
- 处理:优化任务执行速度、调整线程池大小、升级硬件
模式3:线程 BLOCKED 等待锁(内部依赖导致死锁)
"pool-1-thread-1" BLOCKED at MyService.methodA() <- waiting to lock <0x1234>
- locked <0x5678> at MyService.methodB()
"pool-1-thread-2" BLOCKED at MyService.methodB() <- waiting to lock <0x5678>
- locked <0x1234> at MyService.methodA()
- 原因:两个线程互相持有对方需要的锁
- 处理:重新设计锁顺序、使用
tryLock()、避免嵌套同步
模式4:线程 RUNNABLE 但实质卡住(IO/资源阻塞)
"pool-1-thread-1" RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method)
at MyService.callExternalAPI()
at ...
- 原因:网络调用超时未设置或超时过长
- 处理:为远程调用设置合理超时、使用熔断器
具体定位步骤(实战流程)
步骤1:识别线程池工作线程
// 常见线程池命名模式 "http-nio-8080-exec-" // Tomcat "pool-1-thread-" // Executors.newFixedThreadPool(10) "ForkJoinPool.commonPool-worker-" // 并行流 "taskExecutor-" // Spring 自定义线程池
步骤2:分析线程状态分布
# 统计各状态线程数 grep -o '"[^"]*" #[0-9]* .*$' dump.txt | grep -E 'WAITING|BLOCKED|RUNNABLE' | sort | uniq -c # 示例输出 # 12 "pool-1-thread-1" WAITING (parking) # 3 "pool-1-thread-13" RUNNABLE # 1 "main" WAITING (on object monitor)
步骤3:观察堆栈的"等待链"
// 关键模式:等待队列 + 线程池 Thread A: WAITING at LinkedBlockingQueue.take() <- 正常等待新任务 Thread B: RUNNABLE at ExternalService.call() <- 被外部服务阻塞 Thread C: BLOCKED at synchronized block <- 等待内部锁
步骤4:结合多次转储确认阻塞
# 获取两次间隔 5 秒的转储 jstack -l <pid> > dump1.txt sleep 5 jstack -l <pid> > dump2.txt # 对比同一线程的状态变化 grep "pool-1-thread-1" dump*.txt # dump1.txt: "pool-1-thread-1" RUNNABLE at MyService.slowMethod() # dump2.txt: "pool-1-thread-1" RUNNABLE at MyService.slowMethod() # 说明该线程始终在同一个方法中,可能存在阻塞
常见阻塞原因排查表
场景1:工作线程全 WAITING,主线程提交任务阻塞
// 堆栈特征
"main" WAITING at ArrayBlockingQueue.put()
at ThreadPoolExecutor.execute()
解决方案:
// 方案A:使用非阻塞提交
CompletableFuture.runAsync(task)
.exceptionally(ex -> { log.error("提交失败", ex); return null; });
// 方案B:增加队列容量
new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)); // 默认 Integer.MAX_VALUE
// 方案C:拒绝策略降级
new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());
场景2:线程 RUNNABLE 但长时间卡在同一个方法
// 堆栈特征
"pool-1-thread-3" RUNNABLE at MyService.callRemote()
at RestTemplate.execute() // HTTP 调用
at HttpURLConnection.getInputStream()
解决方案:
// 设置超时
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 5秒连接超时
factory.setReadTimeout(10000); // 10秒读取超时
return new RestTemplate(factory);
}
场景3:线程 BLOCKED 等待锁(内部同步)
// 堆栈特征
"pool-1-thread-2" BLOCKED at MyService.updateCache()
- waiting to lock <0x1234> (a java.lang.Object)
解决方案:
// 优化1:缩小同步范围
synchronized (this) {
// 只同步必要的代码块
cache.put(key, value);
}
// 优化2:使用读写锁
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
rwl.readLock().lock(); // 读操作
rwl.writeLock().lock(); // 写操作
// 优化3:无锁数据结构
ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
高级分析技巧
分析线程 CPU 占用
# 获取高 CPU 线程 ID top -H -p <pid> # 将线程 ID 转换为十六进制(与 nid 匹配) printf "%x\n" <tid> # 在转储中搜索 grep "nid=0x<hex_tid>" dump.txt
分析数据库连接池阻塞
// 堆栈特征
"pool-1-thread-1" WAITING at HikariPool.getConnection()
at BasicDataSource.getConnection()
排查:检查数据库最大连接数、慢查询、死锁
分析线程死锁检测
# jstack 会自动检测死锁 Found one Java-level deadlock: ============================= "pool-1-thread-1": waiting to lock <0x1234> at MyService.methodA() but locked <0x5678> by "pool-1-thread-2"
最佳实践建议
预防措施
-
合理配置线程池
// 根据任务类型选择 IO 密集型:CPU核心数 * 2 计算密集型:CPU核心数 + 1 混合型:通过压测确定最佳值
-
线程池监控
// 使用 Micrometer 指标采集 ThreadPoolExecutor executor = new ThreadPoolExecutor(...); new ThreadPoolExecutorMetrics(executor, "myPool", tags);
-
线程命名规范
// 方便转储识别 ThreadFactory namedFactory = new ThreadFactoryBuilder() .setNameFormat("my-task-%d") .build(); new ThreadPoolExecutor(coreSize, maxSize, idleTime, unit, queue, namedFactory);
故障恢复
- 临时缓解:增加线程池核心数(重启应用)
- 长期解决:优化任务逻辑、增加异步化、引入消息队列
- 自动恢复:实现健康检查 + 线程池动态调整
线程转储分析的核心是找到所有线程在等什么,通过观察等待链和资源锁定关系,定位阻塞根源,多次采样对比能排除瞬时状态干扰,结合代码逻辑分析才能找到根本原因。