Java 线程池:复用、Spring 管理、监控与线上故障排查全指南

目录

  1. [多线程池复用 ThreadFactory 与 afterExecute 兜底逻辑](#多线程池复用 ThreadFactory 与 afterExecute 兜底逻辑)
  2. [SpringBoot 多业务隔离线程池实践](#SpringBoot 多业务隔离线程池实践)
  3. [SpringBoot 线程池监控方案](#SpringBoot 线程池监控方案)
  4. 线程池线上完整故障排查流程

一、多线程池复用 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); // 同样调用这一份方法
}
};

关键点:

  1. 异常解析、日志、告警代码只写一份
  2. N 个业务隔离线程池,全部调用这同一个方法;
  3. 如果以后要修改兜底逻辑(比如改告警规则、增加字段),只修改一处,所有线程池全部生效。

❌反面做法:每个线程池复制粘贴一长段 afterExecute 代码。后续改逻辑,要改 N 处,漏改某一个池就会出现:部分线程池异常不会告警,线上 bug。

1.4 总结与易混淆点

**总结一句话:**为了业务隔离,我们会创建多个独立线程池实例,避免一个业务耗尽全部线程。但是我们不会每个线程池重复拷贝 ThreadFactory、afterExecute 的代码。我们把线程工厂封装成通用类,每个线程池实例化它,传入不同线程名称前缀;把 afterExecute 里面 Future 解析、异常日志告警抽成公共工具方法;多个线程池复用同一套底层逻辑,保证所有线程池行为统一,便于维护,避免复制代码带来 bug。

补充区分(很容易混淆):

  1. 不是多个线程池共用同一个 ThreadFactory 对象,而是共用同一套类代码,每个池 new 新实例;
  2. 不是多个线程池共用同一个线程实例,每个线程池自己拥有自己的工作线程;
  3. 只是创建线程的规则、异常处理逻辑代码复用

**举个生活例子:**好比工厂有多条生产线(多个线程池:订单线、报表线):

  • 每条生产线都用同一套标准操作规程(ThreadFactory 类、handleTaskException 公共方法);
  • 但是每条生产线有自己的工人(各自的线程),互不干扰;
  • 操作规程只写一份,改一次,所有生产线全部生效。

二、SpringBoot 多业务隔离线程池实践

2.1 核心要点

  1. 多个独立线程池 Bean(订单、消息消费、报表),互相隔离,避免线程饥饿;
  2. 统一通用线程工厂类,每个 Bean new 实例,传入不同线程前缀;
  3. 抽取公共异常兜底静态方法,所有线程池 afterExecute 调用该方法,不复制重复代码;
  4. 全部交给 Spring 容器管理,支持配置中心配置参数、Micrometer 监控、Spring 优雅销毁;
  5. 可以被 @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 &amp;&amp; r instanceof Future&lt;?&gt;) {
        try {
            Future&lt;?&gt; future = (Future&lt;?&gt;) 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&lt;Runnable&gt; queue = new ArrayBlockingQueue&lt;&gt;(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&lt;Runnable&gt; queue = new ArrayBlockingQueue&lt;&gt;(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&lt;Runnable&gt; queue = new ArrayBlockingQueue&lt;&gt;(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(() -&gt; {
        // 订单异步业务
    });

    // 2. CompletableFuture,传入 Spring 管理的线程池
    CompletableFuture.runAsync(() -&gt; {
        // 报表业务
    }, reportBizThreadPool);
}

// 3. @Async 指定 bean 名称,使用 Spring 托管线程池
@Async("orderBizThreadPool")
public void asyncCreateOrder() {
}
}

2.5 Spring 自动优雅关闭

ThreadPoolExecutor 作为 Spring 的 Bean,Spring 容器销毁的时候,会检测实现 ExecutorService 接口,自动执行 shutdown 流程,不需要自己写 DisposableBean。容器关闭:

  1. 停止接收新任务;
  2. 等待正在执行的任务完成;
  3. 超时后中断任务。

2.6 关键说明与注意点

关键说明:

  1. 多个独立线程池 Bean,每个池拥有自己的线程、队列、参数,业务互相隔离,防止线程饥饿。
  2. BizThreadFactory 是通用类,每个线程池 new BizThreadFactory("biz-xxx")不是共享同一个 ThreadFactory 对象,复用同一套代码逻辑,线程名字前缀区分业务。
  3. commonAfterExecuteHandle 是静态公共方法,所有线程池的 afterExecute 都调用它,异常解析、日志告警只写一份,修改一处全部生效,杜绝复制粘贴代码带来 bug。
  4. 全部交给 Spring 作为 Bean 管理,可以注入、可以被 @AsyncCompletableFuture 使用;接入 micrometer 监控;容器关闭自动优雅停机;参数支持配置中心。

注意点:

  1. 每个线程池都要做监控埋点,Prometheus 可以分别看到每个池的队列长度、活跃线程、拒绝任务数;
  2. 不要把大流量业务和报表这种慢业务放同一个线程池;
  3. commonAfterExecuteHandle 内部逻辑尽量简单,不要执行业务逻辑,防止 afterExecute 内部抛异常导致工作线程销毁。

三、SpringBoot 线程池监控方案

3.1 监控核心指标

两种监控手段:JDK 原生 API 手动采集Micrometer+Prometheus(生产标准)

监控核心看 5 个指标,生产环境必须告警:

  1. 核心线程数、最大线程数
  2. 当前活跃线程数(正在跑任务)
  3. 队列等待任务数量(最关键,积压预警
  4. 已完成任务总数
  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)分开看图表。

告警规则(生产直接抄):

  1. rejected_tasks > 0:触发告警,任务被拒绝;
  2. 队列剩余容量持续 5 分钟小于 10%:任务积压,队列快要打满;
  3. 活跃线程持续等于 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&lt;String,Object&gt; getPoolInfo(){
Map&lt;String,Object&gt; map = new HashMap&lt;&gt;();
map.put("global",buildPool(globalCommonThreadPool));
map.put("order",buildPool(orderBizThreadPool));
return map;
}
private Map&lt;String,Object&gt; buildPool(ThreadPoolExecutor executor){
Map&lt;String,Object&gt; info = new HashMap&lt;&gt;();
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 生产监控最佳实践

  1. 全部使用 Micrometer ExecutorServiceMetrics 埋点,每一个 Spring 托管线程池都要埋点,区分业务标签;
  2. 重点监控拒绝任务数、队列积压、活跃线程数,配置告警。拒绝任务一旦 > 0 必须告警;队列持续上涨代表任务处理不过来;
  3. 不要只看线程数,要看队列 size,很多时候线程没打满,但是队列已经大量积压;
  4. 禁止裸用 JDK ForkJoinPool.commonPool,这个池没有埋点,监控看不到,出问题无法排查;
  5. 配合 jstack,监控看到积压时,导出堆栈,定位线程卡在哪;
  6. 可以开启 actuator 自定义端点,线上快速查看各个线程池实时状态。

3.6 面试高频问题

Q:线程池监控最关注哪几个指标?

活跃线程数、队列等待任务数、拒绝任务数。拒绝任务代表已经打满;队列持续上涨代表任务处理速度跟不上提交速度,会出现业务超时。

Q:为什么 ForkJoinPool.commonPool 不好监控?

JDK 公共池,不属于 Spring 管理,没有自动埋点,拿不到监控指标,出现问题很难定位,所以 CompletableFuture 必须传入自定义线程池。

Q:队列满,但是活跃线程没有达到 maximumPoolSize,什么原因?

ThreadPoolExecutor 执行顺序:核心线程满,优先入队列;队列满才会新建到 max 线程。现象:max20,活跃 8,但是队列已经满。说明核心线程已经耗尽,任务全部塞队列,队列打满之后才会创建更多线程。
提示:如果用无界队列 LinkedBlockingQueue,队列永远不会满,永远不会创建非核心线程,max 参数直接失效。

四、线程池线上完整故障排查流程

4.1 核心思路

线上出问题,核心思路:先看监控定位现象 → 抓现场数据 (jstack、日志) → 判断根因 (流量突增 / 下游慢 / 代码 bug) → 临时止血 → 根治,不盲目调参

4.2 前置知识:线程池执行顺序回顾

  1. 来了任务,优先创建核心线程;
  2. 核心线程满,任务进队列;
  3. 队列满,才创建非核心线程直到 maximumPoolSize
  4. max 线程也打满,触发拒绝策略。

非常高频坑:使用无界队列 LinkedBlockingQueue,队列永远不会满,max 参数直接失效,不会扩容非核心线程,任务无限堆积 OOM。

4.3 场景 1:监控看到队列数量持续上涨(任务积压)

现象:

  • queueSize 不断变大;活跃线程 ≈ max 线程;
  • 接口 / 异步任务耗时变长,业务超时;拒绝任务还没出现。

排查步骤:

  1. 看监控区分:流量上涨,还是单任务执行时间变长
    • QPS 上涨:流量突增;
    • QPS 变化不大,但是任务耗时暴涨:下游变慢(DB、RPC、HTTP)。
  2. 立刻导出 jstack 堆栈,抓现场!最重要一步
shell 复制代码
jstack <pid> > thread.log

搜索自定义线程池的线程名,例如 biz-order-,看每一个线程卡在什么地方:

  • waiting on database:卡在获取数据库连接、慢 SQL;
  • waiting for rpc response:第三方接口响应慢;
  • sleep/lock:代码锁竞争、死锁;

⚠️ 不要等服务重启再 dump,重启现场直接丢失。

**根因分类:**① 下游依赖变慢:DB 慢 SQL、RPC 超时没设置、第三方服务卡顿;② 业务任务本身逻辑重,CPU 计算量大;③ 线程池隔离没做好:某个慢业务占满整个线程池,其他业务任务全部排队(线程饥饿)。

临时止血方案:

  1. 如果是流量突增,评估业务允许的情况下,临时调大 max 线程,增大队列;
  2. 如果是慢业务导致饥饿,紧急隔离,把慢业务切到独立线程池;

❗不要无脑疯狂调大 max 线程,线程过多会 CPU 上下文切换加剧,服务越来越慢。

根治:

  1. 给所有 RPC、DB 调用强制设置超时时间;
  2. 优化慢 SQL、慢业务逻辑;
  3. 做好业务线程池隔离,快慢业务分开;
  4. 告警提前:队列占用超过阈值直接告警,不要等到业务报错才发现。

4.4 场景 2:出现拒绝任务 rejectedTaskCount > 0

**现象:**监控看到拒绝指标上涨,不同拒绝策略表现不一样:

  1. AbortPolicy:大量日志抛出 RejectedExecutionException
  2. CallerRunsPolicy:没有异常,但是调用方线程执行任务,接口 RT 飙升,主线程被占住。

排查步骤:

  1. 判断:是任务处理慢 ,还是流量瞬间打满

很多人第一反应调大线程池参数,治标不治本。

  1. 看 jstack,确认线程池线程全部卡在哪个环节。

临时止血:

  • CallerRunsPolicy:本身就是降级,调用者执行任务,保护线程池;
  • AbortPolicy:紧急可以临时调高 max、队列容量,先恢复业务。

根治:

  1. 如果下游慢:优化下游,增加超时;
  2. 如果是流量峰值:评估扩容,或者接入限流;
  3. 金融业务禁止 Discard 策略,拒绝任务要告警,记录被拒绝的业务数据,避免丢单。

4.5 场景 3:异步任务悄无声息失败,无业务报错

**现象:**业务预期要执行异步逻辑,但是什么都没发生,业务无感知。

根因:

  1. 使用 submit(),异常封装到 Future,业务没有 get;
  2. CompletableFuture 忘记写 exceptionally,并且裸用 JDK ForkJoinPool,没有自定义线程池的 afterExecute 兜底;
  3. @Async 返回 Future,没有捕获异常;
  4. afterExecute 内部代码抛异常,导致工作线程不断销毁重建。

排查:

  1. 查看线程池 afterExecute 打印的全局兜底 error 日志;
  2. 检查是不是任务跑在 JDK 公共 ForkJoinPool,不走自定义线程池,没有兜底逻辑。

根治:

  1. 所有 CompletableFuture 必须传入自定义 Spring 托管线程池;
  2. 保留 afterExecute 全局兜底;业务层自己也要 try‑catch;
  3. afterExecute 内部逻辑尽量简单,不要执行业务逻辑。

4.6 场景 4:线程数莫名暴涨,操作系统线程数打满

现象: 操作系统线程数量持续飙升,top -H 看到大量业务线程。

根因:

  1. 业务代码到处手动 new ThreadPoolExecutor(),没有统一 Spring Bean 管控;
  2. 误用 Executors.newCachedThreadPool()
  3. @Async 使用默认 SimpleAsyncTaskExecutor,每一个任务新建线程。

排查:

  1. jstack 看大量线程名字,看线程来源;
  2. 代码扫描:禁止直接 new ThreadPoolExecutor,全部统一配置 Bean。

根治:

  1. 统一线程池配置,所有线程池交给 Spring 管理;
  2. 代码评审、静态代码检测拦截直接 new 线程池的代码。

4.7 场景 5:服务重启,部分异步任务丢失

**现象:**内存线程池里面正在排队的任务,重启服务直接消失。

根因:

ThreadPoolExecutor 是内存级,队列是 JVM 内存队列,磁盘不持久化。服务 kill,内存队列直接清空。

解决方案:

  1. shutdown 优雅关闭只能处理停机那一刻现存的任务,如果强制 kill -9,任务依然丢失;
  2. 重要业务不能只依赖内存线程池,重要异步任务要使用 MQ 持久化,消息落地,消费端使用线程池执行。

4.8 场景 6:队列满,但是活跃线程没有达到 maxPoolSize

面试高频坑!例如:core=8,max=20,队列 500。core 线程跑满,任务优先进队列;队列没有打满,永远不会创建 8‑20 之间的非核心线程。现象:活跃线程 8,队列已经 480,max=20,但是线程不会扩容。

**原因:**ThreadPoolExecutor 源

相关推荐
脉动数据行情1 小时前
Python WebSocket 实现融通金实时行情监听
开发语言·python·websocket
only-qi1 小时前
Python Agent 开发速通清单
开发语言·python
小蒜学长1 小时前
基于Django的社区团购购物平台的设计与实现(代码+数据库+LW)
数据库·后端·python·django
zq_63891 小时前
ObjectModel_diagram.pdf 文档详细总结
开发语言·matlab
AC赳赳老秦1 小时前
个保法下数据处理:OpenClaw 自动过滤公开数据中的个人信息,保障采集分析合规性
java·python·sqlite·json·php·deepseek·openclaw
Zane19941 小时前
threading、multiprocessing、asyncio 到底怎么选?一张图 + 三组实测数据说清楚
后端·python
Three_ST1 小时前
沐神-动手学习深度学习-习题答案4.4模型选择,欠拟合,过拟合
人工智能·python·深度学习·学习·算法
CodeStats1 小时前
【Java进程通信】Java进程通信系统完全指南:从ProcessBuilder底层原理到多语言实战
java·前端·python·进程·ai编程·processbuilder
用户0332126663672 小时前
使用 Python 设置 Excel 文件属性
python