问题描述
测试小哥直接找上门来了。。
使用 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());
导致问题的核心点
externalUserIds
被 按 100 个一组 分批。- 例如
externalUserIds
有 5000 条数据,则会被拆分成 50 组。 - 每一组都会提交一个异步任务,导致线程池可能同时提交 50 个任务。
- 线程池的队列默认最大是 32,一旦超出,则会抛
RejectedExecutionException
。
为什么本地运行正常,测试环境出错?
- 本地数据量小(如
externalUserIds
只有 300 条,分批后仅 3 组任务)。 - 测试环境数据量大(可能
externalUserIds
有上万条数据,任务数远超 32)。
解决方案
✅ 方案 1:增加队列容量(当前采用的解决方案)
在 ThreadPoolExecutor
配置中,将 queueCapacity
从 32 提高到 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);
}
优点:不会一次性提交所有任务,降低线程池压力。
缺点:任务可能整体执行时间稍长。
总结
-
根本原因:线程池队列容量不足,导致任务被拒绝。
-
最直接的解决方案:增大
queueCapacity
(已实现)。 -
更优的解决方案(推荐):
- 使用
Semaphore
限制并发任务数。 - 按批次执行,避免瞬间提交大量任务。
- 使用
这三种方案可以根据实际业务需求进行选择,以保证系统的稳定性和执行效率。 🚀
今天暂时分享到这,不说了,又有个查询8s的页面等着我去优化。。(留个坑)