线程池任务拒绝异常(RejectedExecutionException)分析与解决方案

45 阅读2分钟

问题描述

测试小哥直接找上门来了。。 使用 ThreadPoolExecutor 执行异步任务时,测试环境报错:

java.util.concurrent.RejectedExecutionException: task java.util.concurrent.FutureTask@1e19e316 rejected
from java.util.concurrent.ThreadPoolExecutor@647b9364[running, pool size = 12, active threads = 12,
queued tasks = 32, completed tasks = 44]

本地运行正常,但测试环境会抛出 RejectedExecutionException

原因分析

代码分析

List<List<String>> partitionedIds = Lists.partition(externalUserIds, 100);
List<CompletableFuture<List<ExternalUserRecord>>> futureList = partitionedIds.stream()
    .map(batch -> CompletableFuture.supplyAsync(
        () -> externalUserRecordService.batchGetExternalUserRecord(batch), executor))
    .collect(Collectors.toList());

导致问题的核心点

  1. externalUserIds按 100 个一组 分批。
  2. 例如 externalUserIds 有 5000 条数据,则会被拆分成 50 组
  3. 每一组都会提交一个异步任务,导致线程池可能同时提交 50 个任务
  4. 线程池的队列默认最大是 32,一旦超出,则会抛 RejectedExecutionException

为什么本地运行正常,测试环境出错?

  • 本地数据量小(如 externalUserIds 只有 300 条,分批后仅 3 组任务)。
  • 测试环境数据量大(可能 externalUserIds 有上万条数据,任务数远超 32)。

解决方案

✅ 方案 1:增加队列容量(当前采用的解决方案)

ThreadPoolExecutor 配置中,将 queueCapacity32 提高到 10000

private static final int QUEUE_CAPACITY = 10000;

@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
    threadPoolTaskExecutor.setCorePoolSize(CORE_POOL_SIZE);
    threadPoolTaskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
    threadPoolTaskExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
    threadPoolTaskExecutor.setQueueCapacity(QUEUE_CAPACITY);
    threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    threadPoolTaskExecutor.setThreadFactory(new CustomizableThreadFactory("excellent-mall-pool-thread-"));
    threadPoolTaskExecutor.initialize();
    return threadPoolTaskExecutor;
}

优点:避免任务被拒绝,保证所有任务都能执行。

缺点:任务全部排队,可能导致等待时间过长。


✅ 方案 2:限制并发任务数

使用 Semaphore 控制 同时执行的任务数,避免一次性提交过多任务:

Semaphore semaphore = new Semaphore(10); // 限制最大并发任务数

List<CompletableFuture<List<ExternalUserRecord>>> futureList = partitionedIds.stream()
    .map(batch -> CompletableFuture.supplyAsync(() -> {
        try {
            semaphore.acquire(); // 获取许可
            return externalUserRecordService.batchGetExternalUserRecord(batch);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return Collections.emptyList();
        } finally {
            semaphore.release(); // 释放许可
        }
    }, executor))
    .collect(Collectors.toList());

优点:控制最大并发任务数,防止线程池超负荷。

缺点:如果任务过多,可能会影响整体执行速度。


✅ 方案 3:分批执行,避免一次性提交所有任务

改为 按批次执行,等待前一批执行完,再提交下一批:

for (List<String> batch : partitionedIds) {
    List<CompletableFuture<List<ExternalUserRecord>>> futureList = batch.stream()
        .map(ids -> CompletableFuture.supplyAsync(
            () -> externalUserRecordService.batchGetExternalUserRecord(ids), executor))
        .collect(Collectors.toList());
    
    // 等待本批任务执行完再提交下一批
    futureList.forEach(CompletableFuture::join);
}

优点:不会一次性提交所有任务,降低线程池压力。

缺点:任务可能整体执行时间稍长。

总结

  1. 根本原因:线程池队列容量不足,导致任务被拒绝。

  2. 最直接的解决方案:增大 queueCapacity(已实现)。

  3. 更优的解决方案(推荐):

    • 使用 Semaphore 限制并发任务数。
    • 按批次执行,避免瞬间提交大量任务。

这三种方案可以根据实际业务需求进行选择,以保证系统的稳定性和执行效率。 🚀

今天暂时分享到这,不说了,又有个查询8s的页面等着我去优化。。(留个坑)