线程池看着简单,日常开发人人都在用,但真正出问题的场景基本都是参数乱配、使用姿势不对导致的。
很多人习惯直接用工具类默认线程池、或者随便写个核心参数,本地测试毫无问题,压测、高并发场景直接雪崩。
记录几个最近线上真实遇到、重复性极高的线程池坑,附带根因和修复代码,可直接对照整改。
- 直接使用 Executors 静态工厂创建线程池
这是阿里开发手册明令禁止的写法,但项目里依旧随处可见。
// 错误写法
ExecutorService executor = Executors.newFixedThreadPool(10);
ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
线上问题根因:
newFixedThreadPool 和 newSingleThreadExecutor 的任务队列是 Integer.MAX_VALUE 无界队列。
高并发、任务堆积时,队列无限扩容,直接导致堆内存飙升、GC 频繁、最终 OOM。
而 newCachedThreadPool 是线程数无上限,突发流量会疯狂创建线程,直接打满 CPU、导致服务卡死。
规范写法:手动定义 ThreadPoolExecutor,指定有界队列和拒绝策略
// 业务自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
5,
10,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
- 核心线程数设置不合理导致的性能瓶颈
很多人有一个误区:CPU 密集型就设核心数为 CPU 核心数,IO 密集型随便开大。
实际业务中大部分接口都是IO 密集型(数据库、Redis、HTTP 调用),线程数过小会导致任务排队、接口 RT 飙升。
但线程数也不能无脑开大:线程过多会带来大量上下文切换,反而拖垮整体性能。
实战配置经验:
- CPU 密集型(计算、解析、排序):核心线程数 = CPU核心数 + 1
- IO 密集型(DB、缓存、网络请求):核心线程数 = CPU核心数 * 2 ~ 4
另外核心线程不要设置过大,非核心线程空闲超时时间务必配置,避免闲置线程长期占用资源。
- 忽略拒绝策略导致任务静默丢失
很多自定义线程池不指定拒绝策略,默认使用 AbortPolicy。
队列满、线程池满载时,直接抛出 RejectedExecutionException,不捕获异常直接任务丢失、接口报错。
还有一部分项目为了不报错,无脑使用 DiscardPolicy,直接丢弃超额任务,无任何日志、无告警,线上问题极难排查。
业务最优方案:
核心业务不允许丢任务,优先使用 CallerRunsPolicy,让主线程自己执行兜底,限流不伤业务;
非核心任务可以自定义拒绝策略,打印日志+埋点告警,方便监控堆积情况。
- 线程池线程未自定义名称,线上排查完全失明
默认线程池线程名称都是 pool-xxx-thread-xxx。
线上日志、线程堆栈、GC 日志中,根本无法区分是哪个业务的线程池在阻塞、在堆积,出问题只能盲猜。
这是非常低成本、高收益的优化点,绝大多数项目都没做好。
正确写法:自定义线程工厂,绑定业务名称
// 自定义线程工厂
ThreadFactory factory = new ThreadFactoryBuilder()
.setNameFormat("order-pool-%d")
.setDaemon(false)
.build();
ThreadPoolExecutor orderThreadPool = new ThreadPoolExecutor(
5,10,60L,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
factory,
new ThreadPoolExecutor.CallerRunsPolicy()
);
出问题直接通过线程名定位到订单业务线程池,排查效率提升数倍。
- 任务内部异常未捕获,线程静默退出
线程池任务抛出未捕获异常时,当前线程会直接终止,线程池会新建一个线程补上。
看似线程池没崩,但实际问题很大:频繁创建销毁线程、任务异常无日志,业务数据错乱完全无感知。
错误示范:
threadPool.execute(() -> {
// 任意空指针、数组越界、数据库异常
int a = 1 / 0;
});
强制规范:所有线程池任务内部必须 try-catch 全部异常,打印详细日志
threadPool.execute(() -> {
try {
int a = 1 / 0;
} catch (Exception e) {
log.error("异步任务执行异常", e);
}
});
- 服务关闭不优雅,异步任务直接中断
Spring 项目停机、发布重启时,线程池未主动关闭,正在执行的异步任务直接被中断,导致数据半成品、业务脏数据。
解决方案:交给 Spring 管理线程池,利用 Bean 销毁钩子优雅关闭
@Bean
public ThreadPoolExecutor businessThreadPool() {
return new ThreadPoolExecutor(5,10,60L,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100));
}
// 项目销毁时关闭线程池
@PreDestroy
public void shutdownPool() {
if (!businessThreadPool.isShutdown()) {
businessThreadPool.shutdown();
try {
if (!businessThreadPool.awaitTermination(3, TimeUnit.SECONDS)) {
businessThreadPool.shutdownNow();
}
} catch (InterruptedException e) {
businessThreadPool.shutdownNow();
}
}
}
小结
线程池的坑基本都不是不会用,而是懒得深究底层规则、习惯套用默认写法。
看似简单的参数、拒绝策略、异常捕获,全部是线上高并发场景的致命关键点。日常开发统一规范线程池创建方式,能规避大部分异步线上故障。