目录
- [多线程池复用 ThreadFactory 与 afterExecute 兜底逻辑](#多线程池复用 ThreadFactory 与 afterExecute 兜底逻辑)
- [SpringBoot 多业务隔离线程池实践](#SpringBoot 多业务隔离线程池实践)
- [SpringBoot 线程池监控方案](#SpringBoot 线程池监控方案)
- 线程池线上完整故障排查流程
一、多线程池复用 ThreadFactory 与 afterExecute 兜底逻辑
1.1 背景与问题
在实际项目中,我们不会整个项目只使用一个线程池 。比如:订单异步池、消息消费池、报表导出池,需要做业务隔离,避免报表慢任务把订单线程吃光(线程饥饿),所以要创建多个独立的 ThreadPoolExecutor 实例。
但是,每个线程池我们都想要:统一格式的线程名字、统一的异常捕获、统一打日志告警。不想每个线程池都复制粘贴一大段 BizThreadFactory、重写一遍 afterExecute 代码。所以核心诉求是:多个线程池实例,复用同一套逻辑代码。
1.2 复用同一个 ThreadFactory(线程工厂)
我们写了一个通用类 BizThreadFactory,职责是给线程设置统一命名前缀、非守护线程、统一优先级。
java
static class BizThreadFactory implements ThreadFactory {
private final AtomicLong threadNum = new AtomicLong(0);
private final String threadNamePrefix;
public BizThreadFactory(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, threadNamePrefix + "-" + threadNum.getAndIncrement());
t.setDaemon(false);
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
复用逻辑:类只写一份,每个线程池 new 这个类的对象,传入不同前缀。
java
// 线程池1:订单线程池
ThreadFactory orderThreadFactory = new BizThreadFactory("biz-order");
// 线程池2:报表线程池
ThreadFactory reportThreadFactory = new BizThreadFactory("biz-report");
- 两份线程工厂对象,来自同一个类代码,逻辑完全一样;
- 仅仅线程名字前缀不一样;
- 好处:所有线程命名风格统一,jstack、日志排查一眼分清属于哪个业务池。
❌反例:每个线程池自己手写一套 ThreadFactory,代码重复,有的忘了命名、有的设成守护线程,线上排查混乱。
1.3 复用 afterExecute 异常兜底逻辑
afterExecute 是 ThreadPoolExecutor 的钩子方法,写在匿名子类里面。如果有 3 个线程池,不做复用就要复制 3 遍一大段 afterExecute 处理 Future、异常打日志告警代码。
方案:把兜底逻辑抽成公共静态方法,所有线程池都调用这一份逻辑。
java
/**
* 公共统一异常处理逻辑,只写一份,所有线程池复用
*/
private static void handleTaskException(Runnable r, Throwable t) {
Throwable realEx = t;
if (realEx == null && r instanceof Future<?>) {
try {
Future<?> future = (Future<?>) r;
if (future.isDone()) {
future.get();
}
} catch (CancellationException ce) {
realEx = ce;
} catch (ExecutionException ee) {
realEx = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
if (realEx != null) {
log.error("[线程池全局兜底]任务异常", realEx);
//alertService.alert(...)
}
}
然后,每一个不同业务的线程池,重写 afterExecute 的时候直接调用这个公共方法:
java
// 订单线程池
ThreadPoolExecutor orderPool = new ThreadPoolExecutor(...) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
handleTaskException(r, t); // 调用公共复用逻辑
}
};
// 报表线程池
ThreadPoolExecutor reportPool = new ThreadPoolExecutor(...) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
handleTaskException(r, t); // 同样调用这一份方法
}
};
关键点:
- 异常解析、日志、告警代码只写一份;
- N 个业务隔离线程池,全部调用这同一个方法;
- 如果以后要修改兜底逻辑(比如改告警规则、增加字段),只修改一处,所有线程池全部生效。
❌反面做法:每个线程池复制粘贴一长段 afterExecute 代码。后续改逻辑,要改 N 处,漏改某一个池就会出现:部分线程池异常不会告警,线上 bug。
1.4 总结与易混淆点
**总结一句话:**为了业务隔离,我们会创建多个独立线程池实例,避免一个业务耗尽全部线程。但是我们不会每个线程池重复拷贝 ThreadFactory、afterExecute 的代码。我们把线程工厂封装成通用类,每个线程池实例化它,传入不同线程名称前缀;把 afterExecute 里面 Future 解析、异常日志告警抽成公共工具方法;多个线程池复用同一套底层逻辑,保证所有线程池行为统一,便于维护,避免复制代码带来 bug。
补充区分(很容易混淆):
- 不是多个线程池共用同一个 ThreadFactory 对象,而是共用同一套类代码,每个池 new 新实例;
- 不是多个线程池共用同一个线程实例,每个线程池自己拥有自己的工作线程;
- 只是创建线程的规则、异常处理逻辑代码复用。
**举个生活例子:**好比工厂有多条生产线(多个线程池:订单线、报表线):
- 每条生产线都用同一套标准操作规程(ThreadFactory 类、handleTaskException 公共方法);
- 但是每条生产线有自己的工人(各自的线程),互不干扰;
- 操作规程只写一份,改一次,所有生产线全部生效。
二、SpringBoot 多业务隔离线程池实践
2.1 核心要点
- 多个独立线程池 Bean(订单、消息消费、报表),互相隔离,避免线程饥饿;
- 统一通用线程工厂类,每个 Bean new 实例,传入不同线程前缀;
- 抽取公共异常兜底静态方法,所有线程池
afterExecute调用该方法,不复制重复代码; - 全部交给 Spring 容器管理,支持配置中心配置参数、Micrometer 监控、Spring 优雅销毁;
- 可以被
@Async("beanName")、CompletableFuture直接使用。
2.2 完整配置代码
java
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.executor.ExecutorServiceMetrics;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.annotation.Resource;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@Configuration
public class MultiBizThreadPoolConfig {
@Resource
private MeterRegistry meterRegistry;
// ====================== 通用可复用组件 ======================
/**
* 通用线程工厂,所有线程池复用这个类代码,每个池 new 新对象,传入不同前缀
*/
public static class BizThreadFactory implements ThreadFactory {
private final AtomicLong threadNum = new AtomicLong(0);
private final String threadNamePrefix;
public BizThreadFactory(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, threadNamePrefix + "-" + threadNum.getAndIncrement());
t.setDaemon(false);
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
/**
* 公共统一 afterExecute 兜底逻辑【只写一份,所有线程池复用】
*/
private static void commonAfterExecuteHandle(Runnable r, Throwable t) {
Throwable realEx = t;
if (realEx == null && r instanceof Future<?>) {
try {
Future<?> future = (Future<?>) r;
if (future.isDone()) {
future.get();
}
} catch (CancellationException ce) {
realEx = ce;
} catch (ExecutionException ee) {
realEx = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
if (realEx != null) {
log.error("[线程池全局兜底]任务执行异常", realEx);
// 此处接入告警服务 alertService.alert(...)
}
}
// ====================== 业务1:通用全局业务线程池 Bean ======================
@Value("${thread-pool.global.core:8}")
private int globalCore;
@Value("${thread-pool.global.max:20}")
private int globalMax;
@Value("${thread-pool.global.queue:500}")
private int globalQueue;
@Value("${thread-pool.global.keepAlive:60}")
private long globalKeepAlive;
@Primary
@Bean("globalCommonThreadPool")
public ThreadPoolExecutor globalCommonThreadPool() {
ThreadFactory factory = new BizThreadFactory("biz-global");
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(globalQueue);
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
globalCore, globalMax, globalKeepAlive, TimeUnit.SECONDS,
queue, factory, handler
) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
commonAfterExecuteHandle(r, t);
}
};
ExecutorServiceMetrics.monitor(meterRegistry, executor, "thread.pool.global");
return executor;
}
// ====================== 业务2:订单业务专用线程池 Bean ======================
@Value("${thread-pool.order.core:4}")
private int orderCore;
@Value("${thread-pool.order.max:12}")
private int orderMax;
@Value("${thread-pool.order.queue:200}")
private int orderQueue;
@Value("${thread-pool.order.keepAlive:60}")
private long orderKeepAlive;
@Bean("orderBizThreadPool")
public ThreadPoolExecutor orderBizThreadPool() {
ThreadFactory factory = new BizThreadFactory("biz-order");
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(orderQueue);
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
orderCore, orderMax, orderKeepAlive, TimeUnit.SECONDS,
queue, factory, handler
) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
commonAfterExecuteHandle(r, t);
}
};
ExecutorServiceMetrics.monitor(meterRegistry, executor, "thread.pool.order");
return executor;
}
// ====================== 业务3:报表导出专用线程池 Bean ======================
@Value("${thread-pool.report.core:2}")
private int reportCore;
@Value("${thread-pool.report.max:6}")
private int reportMax;
@Value("${thread-pool.report.queue:100}")
private int reportQueue;
@Value("${thread-pool.report.keepAlive:60}")
private long reportKeepAlive;
@Bean("reportBizThreadPool")
public ThreadPoolExecutor reportBizThreadPool() {
ThreadFactory factory = new BizThreadFactory("biz-report");
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(reportQueue);
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
reportCore, reportMax, reportKeepAlive, TimeUnit.SECONDS,
queue, factory, handler
) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
commonAfterExecuteHandle(r, t);
}
};
ExecutorServiceMetrics.monitor(meterRegistry, executor, "thread.pool.report");
return executor;
}
}
2.3 application.yml 配置(可放到 Nacos 配置中心动态配置)
yaml
thread-pool:
global:
core: 8
max: 20
queue: 500
keepAlive: 60
order:
core: 4
max: 12
queue: 200
keepAlive: 60
report:
core: 2
max: 6
queue: 100
keepAlive: 60
2.4 使用方式(Spring 容器管理,直接注入)
java
@Service
public class BizService {
// 通用池
@Resource(name = "globalCommonThreadPool")
private ThreadPoolExecutor globalCommonThreadPool;
// 订单专用池
@Resource(name = "orderBizThreadPool")
private ThreadPoolExecutor orderBizThreadPool;
// 报表专用池
@Resource(name = "reportBizThreadPool")
private ThreadPoolExecutor reportBizThreadPool;
public void test() {
// 1. 直接 execute/submit
orderBizThreadPool.submit(() -> {
// 订单异步业务
});
// 2. CompletableFuture,传入 Spring 管理的线程池
CompletableFuture.runAsync(() -> {
// 报表业务
}, reportBizThreadPool);
}
// 3. @Async 指定 bean 名称,使用 Spring 托管线程池
@Async("orderBizThreadPool")
public void asyncCreateOrder() {
}
}
2.5 Spring 自动优雅关闭
ThreadPoolExecutor作为 Spring 的 Bean,Spring 容器销毁的时候,会检测实现ExecutorService接口,自动执行 shutdown 流程,不需要自己写 DisposableBean。容器关闭:
- 停止接收新任务;
- 等待正在执行的任务完成;
- 超时后中断任务。
2.6 关键说明与注意点
关键说明:
- 多个独立线程池 Bean,每个池拥有自己的线程、队列、参数,业务互相隔离,防止线程饥饿。
BizThreadFactory是通用类,每个线程池new BizThreadFactory("biz-xxx"),不是共享同一个 ThreadFactory 对象,复用同一套代码逻辑,线程名字前缀区分业务。commonAfterExecuteHandle是静态公共方法,所有线程池的 afterExecute 都调用它,异常解析、日志告警只写一份,修改一处全部生效,杜绝复制粘贴代码带来 bug。- 全部交给 Spring 作为 Bean 管理,可以注入、可以被
@Async、CompletableFuture使用;接入 micrometer 监控;容器关闭自动优雅停机;参数支持配置中心。
注意点:
- 每个线程池都要做监控埋点,Prometheus 可以分别看到每个池的队列长度、活跃线程、拒绝任务数;
- 不要把大流量业务和报表这种慢业务放同一个线程池;
commonAfterExecuteHandle内部逻辑尽量简单,不要执行业务逻辑,防止 afterExecute 内部抛异常导致工作线程销毁。
三、SpringBoot 线程池监控方案
3.1 监控核心指标
两种监控手段:JDK 原生 API 手动采集 、Micrometer+Prometheus(生产标准)。
监控核心看 5 个指标,生产环境必须告警:
- 核心线程数、最大线程数
- 当前活跃线程数(正在跑任务)
- 队列等待任务数量(最关键,积压预警)
- 已完成任务总数
- 拒绝任务数量(出现就代表流量打满,要告警)
重点:多个 Spring 托管线程池 Bean,每个线程池都要独立监控,不能只监控一个。
3.2 方式一:Micrometer + Prometheus(推荐,企业生产)
前面代码中这一行就是埋点:
java
ExecutorServiceMetrics.monitor(meterRegistry, executor, "thread.pool.order");
把 ThreadPoolExecutor 交给 ExecutorServiceMetrics,Micrometer 会自动定时采集线程池全部状态指标,暴露给 Prometheus。
会自动生成的核心指标:
| 指标 | 含义 | 告警建议 |
|---|---|---|
thread_pool_threads_active |
活跃正在执行任务线程数 | 接近 max 线程数告警 |
thread_pool_queue_remaining |
队列剩余容量 | 剩余很小,队列快满告警 |
thread_pool_completed_tasks |
完成任务总数 | 看吞吐量 |
thread_pool_rejected_tasks |
被拒绝任务总数 | 大于 0 直接告警,线上事故 |
thread_pool_pool_size |
当前实际线程数量 | 观测线程伸缩 |
Grafana 可以直接做线程池大盘:每个业务池(global/order/report)分开看图表。
告警规则(生产直接抄):
rejected_tasks > 0:触发告警,任务被拒绝;- 队列剩余容量持续 5 分钟小于 10%:任务积压,队列快要打满;
- 活跃线程持续等于 maximumPoolSize:线程全部打满。
⚠️注意:如果 CompletableFuture 裸用 ForkJoinPool.commonPool,这个池没有埋点,看不到监控,这就是为什么强制要求传入自定义线程池。
3.3 方式二:JDK 原生 API,手动获取线程池状态
ThreadPoolExecutor 自带一堆 get 方法,不需要引入组件,可以写接口 / 定时打印日志。
java
ThreadPoolExecutor pool;
//当前正在执行任务的线程
int activeCount = pool.getActiveCount();
//队列中等待任务数量
int queueSize = pool.getQueue().size();
//已经完成任务总数
long completedTaskCount = pool.getCompletedTaskCount();
//总共拒绝的任务
long rejectedCount = pool.getRejectedTaskCount();
//当前池里面存活线程数目
int poolSize = pool.getPoolSize();
//核心线程、最大线程
int core = pool.getCorePoolSize();
int max = pool.getMaximumPoolSize();
示例:写个定时任务打印全部 Spring 托管线程池状态(适合开发调试,生产还是优先 micrometer):
java
@Scheduled(fixedRate = 5000)
public void printThreadPoolStatus(){
log.info("[global池]active:{},queueSize:{},rejected:{}",
globalCommonThreadPool.getActiveCount(),
globalCommonThreadPool.getQueue().size(),
globalCommonThreadPool.getRejectedTaskCount());
log.info("[order池]active:{},queueSize:{},rejected:{}",
orderBizThreadPool.getActiveCount(),
orderBizThreadPool.getQueue().size(),
orderBizThreadPool.getRejectedTaskCount());
}
3.4 方式三:SpringBoot Actuator + 自定义端点
自定义 actuator 端点,线上可以 http 调用直接拿到所有线程池状态,方便排查。
java
@RestControllerEndpoint(id = "threadpool")
public class ThreadPoolActuatorEndpoint {
@Resource(name = "globalCommonThreadPool")
private ThreadPoolExecutor globalCommonThreadPool;
@Resource(name = "orderBizThreadPool")
private ThreadPoolExecutor orderBizThreadPool;
@ReadOperation
public Map<String,Object> getPoolInfo(){
Map<String,Object> map = new HashMap<>();
map.put("global",buildPool(globalCommonThreadPool));
map.put("order",buildPool(orderBizThreadPool));
return map;
}
private Map<String,Object> buildPool(ThreadPoolExecutor executor){
Map<String,Object> info = new HashMap<>();
info.put("activeCount",executor.getActiveCount());
info.put("queueSize",executor.getQueue().size());
info.put("rejectedTaskCount",executor.getRejectedTaskCount());
info.put("poolSize",executor.getPoolSize());
info.put("corePoolSize",executor.getCorePoolSize());
info.put("maxPoolSize",executor.getMaximumPoolSize());
return info;
}
}
访问地址:/actuator/threadpool,直接返回各个线程池 JSON 状态。
3.5 生产监控最佳实践
- 全部使用 Micrometer ExecutorServiceMetrics 埋点,每一个 Spring 托管线程池都要埋点,区分业务标签;
- 重点监控拒绝任务数、队列积压、活跃线程数,配置告警。拒绝任务一旦 > 0 必须告警;队列持续上涨代表任务处理不过来;
- 不要只看线程数,要看队列 size,很多时候线程没打满,但是队列已经大量积压;
- 禁止裸用 JDK ForkJoinPool.commonPool,这个池没有埋点,监控看不到,出问题无法排查;
- 配合 jstack,监控看到积压时,导出堆栈,定位线程卡在哪;
- 可以开启 actuator 自定义端点,线上快速查看各个线程池实时状态。
3.6 面试高频问题
Q:线程池监控最关注哪几个指标?
活跃线程数、队列等待任务数、拒绝任务数。拒绝任务代表已经打满;队列持续上涨代表任务处理速度跟不上提交速度,会出现业务超时。
Q:为什么 ForkJoinPool.commonPool 不好监控?
JDK 公共池,不属于 Spring 管理,没有自动埋点,拿不到监控指标,出现问题很难定位,所以 CompletableFuture 必须传入自定义线程池。
Q:队列满,但是活跃线程没有达到 maximumPoolSize,什么原因?
ThreadPoolExecutor 执行顺序:核心线程满,优先入队列;队列满才会新建到 max 线程。现象:max20,活跃 8,但是队列已经满。说明核心线程已经耗尽,任务全部塞队列,队列打满之后才会创建更多线程。
提示:如果用无界队列LinkedBlockingQueue,队列永远不会满,永远不会创建非核心线程,max 参数直接失效。
四、线程池线上完整故障排查流程
4.1 核心思路
线上出问题,核心思路:先看监控定位现象 → 抓现场数据 (jstack、日志) → 判断根因 (流量突增 / 下游慢 / 代码 bug) → 临时止血 → 根治,不盲目调参
4.2 前置知识:线程池执行顺序回顾
- 来了任务,优先创建核心线程;
- 核心线程满,任务进队列;
- 队列满,才创建非核心线程直到 maximumPoolSize;
- max 线程也打满,触发拒绝策略。
非常高频坑:使用无界队列 LinkedBlockingQueue,队列永远不会满,max 参数直接失效,不会扩容非核心线程,任务无限堆积 OOM。
4.3 场景 1:监控看到队列数量持续上涨(任务积压)
现象:
- queueSize 不断变大;活跃线程 ≈ max 线程;
- 接口 / 异步任务耗时变长,业务超时;拒绝任务还没出现。
排查步骤:
- 看监控区分:流量上涨,还是单任务执行时间变长
- QPS 上涨:流量突增;
- QPS 变化不大,但是任务耗时暴涨:下游变慢(DB、RPC、HTTP)。
- 立刻导出 jstack 堆栈,抓现场!最重要一步
shell
jstack <pid> > thread.log
搜索自定义线程池的线程名,例如 biz-order-,看每一个线程卡在什么地方:
waiting on database:卡在获取数据库连接、慢 SQL;waiting for rpc response:第三方接口响应慢;sleep/lock:代码锁竞争、死锁;
⚠️ 不要等服务重启再 dump,重启现场直接丢失。
**根因分类:**① 下游依赖变慢:DB 慢 SQL、RPC 超时没设置、第三方服务卡顿;② 业务任务本身逻辑重,CPU 计算量大;③ 线程池隔离没做好:某个慢业务占满整个线程池,其他业务任务全部排队(线程饥饿)。
临时止血方案:
- 如果是流量突增,评估业务允许的情况下,临时调大 max 线程,增大队列;
- 如果是慢业务导致饥饿,紧急隔离,把慢业务切到独立线程池;
❗不要无脑疯狂调大 max 线程,线程过多会 CPU 上下文切换加剧,服务越来越慢。
根治:
- 给所有 RPC、DB 调用强制设置超时时间;
- 优化慢 SQL、慢业务逻辑;
- 做好业务线程池隔离,快慢业务分开;
- 告警提前:队列占用超过阈值直接告警,不要等到业务报错才发现。
4.4 场景 2:出现拒绝任务 rejectedTaskCount > 0
**现象:**监控看到拒绝指标上涨,不同拒绝策略表现不一样:
AbortPolicy:大量日志抛出RejectedExecutionException;CallerRunsPolicy:没有异常,但是调用方线程执行任务,接口 RT 飙升,主线程被占住。
排查步骤:
- 判断:是任务处理慢 ,还是流量瞬间打满。
很多人第一反应调大线程池参数,治标不治本。
- 看 jstack,确认线程池线程全部卡在哪个环节。
临时止血:
- CallerRunsPolicy:本身就是降级,调用者执行任务,保护线程池;
- AbortPolicy:紧急可以临时调高 max、队列容量,先恢复业务。
根治:
- 如果下游慢:优化下游,增加超时;
- 如果是流量峰值:评估扩容,或者接入限流;
- 金融业务禁止 Discard 策略,拒绝任务要告警,记录被拒绝的业务数据,避免丢单。
4.5 场景 3:异步任务悄无声息失败,无业务报错
**现象:**业务预期要执行异步逻辑,但是什么都没发生,业务无感知。
根因:
- 使用
submit(),异常封装到 Future,业务没有 get; - CompletableFuture 忘记写 exceptionally,并且裸用 JDK ForkJoinPool,没有自定义线程池的 afterExecute 兜底;
- @Async 返回 Future,没有捕获异常;
- afterExecute 内部代码抛异常,导致工作线程不断销毁重建。
排查:
- 查看线程池
afterExecute打印的全局兜底 error 日志; - 检查是不是任务跑在 JDK 公共 ForkJoinPool,不走自定义线程池,没有兜底逻辑。
根治:
- 所有 CompletableFuture 必须传入自定义 Spring 托管线程池;
- 保留 afterExecute 全局兜底;业务层自己也要 try‑catch;
- afterExecute 内部逻辑尽量简单,不要执行业务逻辑。
4.6 场景 4:线程数莫名暴涨,操作系统线程数打满
现象: 操作系统线程数量持续飙升,top -H 看到大量业务线程。
根因:
- 业务代码到处手动
new ThreadPoolExecutor(),没有统一 Spring Bean 管控; - 误用
Executors.newCachedThreadPool(); - @Async 使用默认
SimpleAsyncTaskExecutor,每一个任务新建线程。
排查:
- jstack 看大量线程名字,看线程来源;
- 代码扫描:禁止直接 new ThreadPoolExecutor,全部统一配置 Bean。
根治:
- 统一线程池配置,所有线程池交给 Spring 管理;
- 代码评审、静态代码检测拦截直接 new 线程池的代码。
4.7 场景 5:服务重启,部分异步任务丢失
**现象:**内存线程池里面正在排队的任务,重启服务直接消失。
根因:
ThreadPoolExecutor 是内存级,队列是 JVM 内存队列,磁盘不持久化。服务 kill,内存队列直接清空。
解决方案:
- shutdown 优雅关闭只能处理停机那一刻现存的任务,如果强制 kill -9,任务依然丢失;
- 重要业务不能只依赖内存线程池,重要异步任务要使用 MQ 持久化,消息落地,消费端使用线程池执行。
4.8 场景 6:队列满,但是活跃线程没有达到 maxPoolSize
面试高频坑!例如:core=8,max=20,队列 500。core 线程跑满,任务优先进队列;队列没有打满,永远不会创建 8‑20 之间的非核心线程。现象:活跃线程 8,队列已经 480,max=20,但是线程不会扩容。
**原因:**ThreadPoolExecutor 源