做后台开发这些年,最怕的不是代码写不出来,而是线上突然告警:接口响应变慢,CPU飙升。查来查去,最后发现是某个任务把线程池堵死了。这种情况在高并发场景下太常见了,尤其是涉及IO操作、远程调用或者数据库查询时,一不小心就卡在线程池里。
问题从哪来?
举个例子,公司有个订单导出功能,用户上传一批订单号,系统挨个查数据库再打包返回。一开始用的是固定大小的线程池,10个线程处理请求。结果某天运营搞促销,一下子涌进来几百个导出任务,每个任务又包含上百个订单查询,线程全被占满,后续所有请求直接卡住——这就是典型的阻塞堆积。
线程池本身不会主动拒绝合理的任务,但当任务执行时间远超预期,或者资源竞争激烈时,队列越积越长,最终拖垮整个服务。
怎么让线程池“聪明”起来?
核心思路不是一味扩大线程数,而是做好“管理+监控+兜底”。我们后来做了几项调整:
第一,换用带拒绝策略的线程池。比如使用 ThreadPoolExecutor 时,明确指定队列类型和拒绝行为:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10,
20,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
这里用了有界队列,最多容纳100个待处理任务。一旦超出,由提交任务的主线程自己执行——虽然会拖慢当前请求,但能防止其他线程被耗尽,相当于给系统上了个“熔断阀”。
加点“超时控制”更稳妥
有些任务本身就是“慢性子”,比如调第三方接口。这时候得主动设限。我们对所有远程调用加上超时:
Future<String> future = executor.submit(() -> {
// 模拟远程请求
return remoteService.getData();
});
try {
String result = future.get(3, TimeUnit.SECONDS); // 最多等3秒
} catch (TimeoutException e) {
future.cancel(true); // 取消任务,释放线程
}
这样即使外部服务抽风,也不会把我们的线程长期锁住。
监控不能少
光改代码还不够,还得看得见。我们在 Prometheus 里暴露了几个关键指标:活跃线程数、队列长度、已完成任务数。配合 Grafana 做成面板,运维同事一眼就能看出有没有异常堆积。
有次凌晨报警,就是队列长度突增到90以上。排查发现是某个定时任务忘了加超时,跑了一个小时没结束。及时修复后,系统很快恢复正常。
别忽视任务本身的优化
有时候问题不在线程池,而在任务设计。比如原本是逐条查数据库,改成批量查询后,单个任务执行时间从5秒降到800毫秒,线程利用率直接提升六倍。这种优化比调参数更治本。
后来我们还引入了虚拟线程(Java 19+)试了试,在高并发IO场景下确实轻松不少,但目前还在灰度,老项目还是以稳定为主。
线程池不是扔进去就完事的黑盒,它需要持续观察和动态调整。特别是在业务快速增长期,昨天还够用的配置,今天可能就成了瓶颈。多留个心眼,往往能避开一次线上事故。