从一次线上崩溃学到的:Java 线程池千万别这样用
上个月线上服务突然卡死,排查半天发现是线程池用错了。记录一下,避免后人踩坑。 看起来没问题?问题在 process 方法里。 process 内部也加超时,不依赖外部服务良心: 更深的教训 这样队列满的时候,主线程自己跑任务,自然降速,不会压垮系统。 线程池里的任务尽量别阻塞,快速处理完释放 future.get() 必须设超时,不然就是埋雷 别用 Executors 的便捷方法,生产环境自己配 ThreadPoolExecutor ,队列要有界 拒绝策略想清楚,是丢弃、抛异常、还是让调用方自己执行
现象
早上 9 点用户高峰期,服务响应越来越慢,最后直接无响应。重启后恢复,过半小时又卡死。
日志里没有报错,CPU 占用不高,内存也正常。就是所有请求都超时。
排查
dump 线程栈一看,所有线程都阻塞在提交任务的地方。
代码简化后是这样:ExecutorService executor = Executors.newFixedThreadPool(10);
public void handleRequest(Request req) {
Future<Result> future = executor.submit(() -> process(req));
Result result = future.get(); // 阻塞等待
}
process 内部又调了另一个服务,那个服务超时了。线程池里的 10 个线程全被占满,都在等外部响应。
新请求进来,往线程池提交任务,队列塞满,最后拒绝。但 future.get() 还在阻塞等结果,整个链路卡死。
根本原因
线程池里的线程不是越多越好,但也不是固定就够用。
这里两个错误:
线程池里的线程应该快速处理完释放,而不是长时间挂起等 IO。
future.get() 没有设超时,无限等下去。
修复
改成异步 + 超时:Future<Result> future = executor.submit(() -> process(req));
try {
Result result = future.get(2, TimeUnit.SECONDS); // 最多等2秒
} catch (TimeoutException e) {
future.cancel(true); // 取消任务,释放线程
return fallbackResult(req);
}public Result process(Request req) {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1))
.build();
// 请求外部服务,最多等1秒
Response resp = client.send(request, BodyHandlers.ofString());
return parse(resp);
}
Executors.newFixedThreadPool 底层用的是无界队列 LinkedBlockingQueue 。
任务提交速度超过处理速度,队列无限增长,最后内存溢出。
线上改用自定义线程池,有界队列 + 拒绝策略:ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(100), // 有界队列,最多100个排队
new ThreadPoolExecutor.CallerRunsPolicy() // 满了让提交者自己执行
);
总结
你们线程池踩过什么坑?评论区交流。