一、ThreadPoolExecutor vs ThreadPoolTaskExecutor
目录
本质关系
从原理看区别
从方法看区别
从实际使用看区别
总结一句话
二、公共池是谁在用?为什么一个任务慢会卡住其他的?
公共池是谁创建的?
公共池在哪?
为什么一个任务慢会卡住其他的?
实际项目中的隐患
所以为什么不要用公共池
总结
本质关系
ThreadPoolTaskExecutor 是 Spring 对 ThreadPoolExecutor 的封装
┌─────────────────────────────────────────┐ │ ThreadPoolTaskExecutor │ ← Spring封装层 │ 管理生命周期、支持YAML配置、支持@Async │ │ │ │ ┌───────────────────────────────────┐ │ │ │ ThreadPoolExecutor │ │ ← JDK原生 │ │ 线程池核心逻辑: │ │ │ │ - 线程调度 │ │ │ │ - 队列管理 │ │ │ │ - 拒绝策略 │ │ │ └───────────────────────────────────┘ │ └─────────────────────────────────────────┘
从原理看区别
| 维度 | ThreadPoolExecutor | ThreadPoolTaskExecutor |
|---|---|---|
| 来源 | JDK原生(java.util.concurrent) | Spring封装(spring-context) |
| 内部实现 | 直接管理线程和队列 | 内部持有一个ThreadPoolExecutor |
| 生命周期 | 需要手动shutdown | Spring自动管理(@Bean销毁时自动关闭) |
| 配置方式 | Java代码硬编码 | 支持application.yml配置 |
| 任务装饰 | 无 | 支持TaskDecorator(给任务加前后逻辑) |
| 和Spring集成 | 无感知 | 与@Async、@EnableAsync深度集成 |
| 线程命名 | 需要自己写ThreadFactory | setThreadNamePrefix一行搞定 |
从方法看区别
ThreadPoolTaskExecutor 额外提供的方法:
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); // ① 线程名前缀(ThreadPoolExecutor要自己写ThreadFactory) taskExecutor.setThreadNamePrefix("async-"); // 效果:线程名变成 async-1, async-2, async-3 // ② 关闭时等待任务完成(ThreadPoolExecutor要手动shutdown+awaitTermination) taskExecutor.setWaitForTasksToCompleteOnShutdown(true); taskExecutor.setAwaitTerminationSeconds(30); // ③ 任务装饰器(给每个任务执行前后加逻辑) taskExecutor.setTaskDecorator(r -> { // 保存当前线程的上下文(比如MDC、ThreadLocal) MDC.put("traceId", UUID.randomUUID().toString()); return () -> { try { r.run(); } finally { MDC.clear(); } }; }); // ④ 手动执行(Spring特有) taskExecutor.execute(() -> doSomething()); // ⑤ 停止后不接受新任务 taskExecutor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy()); // 应用启动后才会初始化,初始化后返回内部的ThreadPoolExecutor taskExecutor.initialize(); // 获取内部原生的ThreadPoolExecutor(需要时可以拿到) ThreadPoolExecutor raw = taskExecutor.getThreadPoolExecutor();ThreadPoolExecutor 没有以上方法,只有:
ThreadPoolExecutor executor = new ThreadPoolExecutor(...); executor.execute(task); // 执行 executor.submit(task); // 提交带返回值 executor.shutdown(); // 不接受新任务,等已提交的完成 executor.shutdownNow(); // 立即停止,返回未执行的任务列表 executor.isShutdown(); // 是否已调用shutdown executor.isTerminated(); // 是否所有任务都已完成 executor.getActiveCount(); // 当前活跃线程数 executor.getQueue().size(); // 队列中等待的任务数从实际使用看区别
场景一:项目标准配置(推荐用ThreadPoolTaskExecutor)
@Configuration @EnableAsync public class ThreadPoolConfig { @Bean("asyncExecutor") public ThreadPoolTaskExecutor asyncExecutor() { ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor(); e.setCorePoolSize(8); e.setMaxPoolSize(16); e.setQueueCapacity(200); e.setThreadNamePrefix("async-"); e.setWaitForTasksToCompleteOnShutdown(true); e.setAwaitTerminationSeconds(30); e.initialize(); return e; } } // 业务代码 @Service public class OrderService { @Async("asyncExecutor") public void sendNotification(Long orderId) { ... } }场景二:需要精细控制时,拿内部原生对象
@Autowired private ThreadPoolTaskExecutor taskExecutor; public void someMethod() { // 拿到内部的ThreadPoolExecutor ThreadPoolExecutor raw = taskExecutor.getThreadPoolExecutor(); // 可以动态调整线程数 raw.setCorePoolSize(16); raw.setMaximumPoolSize(32); // 可以实时监控 log.info("活跃线程:{}", raw.getActiveCount()); log.info("队列积压:{}", raw.getQueue().size()); log.info("已完成任务:{}", raw.getCompletedTaskCount()); }场景三:不用Spring的通用Java项目(用ThreadPoolExecutor)
// 没有Spring,没有@Configuration,纯Java ThreadPoolExecutor executor = new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), r -> new Thread(r, "worker-" + counter.incrementAndGet()) ); executor.execute(() -> doSomething()); // 应用关闭时手动销毁 Runtime.getRuntime().addShutdownHook(new Thread(() -> { executor.shutdown(); executor.awaitTermination(10, TimeUnit.SECONDS); }));总结一句话
| 场景 | 用什么 |
|---|---|
| Spring Boot项目 | ThreadPoolTaskExecutor+@Async |
| 通用Java项目、工具类库 | ThreadPoolExecutor |
| 需要动态调整线程数/监控 | ThreadPoolTaskExecutor+getThreadPoolExecutor() |
二、公共池是谁在用?为什么一个任务慢会卡住其他的?
公共池是谁创建的?
不是JVM自动创建的。是你调用了某些API时,JVM懒加载创建的。
// 这两个操作会触发公共池创建 CompletableFuture.supplyAsync(() -> ...); // 不指定线程池时 list.parallelStream().map(...); // 并行流触发时机:
// 第一次调用时,JVM内部创建公共池 ForkJoinPool commonPool = ForkJoinPool.commonPool(); // 线程数 = Runtime.getRuntime().availableProcessors() - 1 // 比如4核CPU → 3个线程,8核 → 7个线程公共池在哪?
┌──────────────────────────────────────────┐ │ JVM 进程 │ │ │ │ ┌──────────────────────────────────┐ │ │ │ ForkJoinPool.commonPool() │ │ ← 全局唯一,静态实例 │ │ 3个线程(4核CPU) │ │ │ │ │ │ │ │ 谁在用? │ │ │ │ ① CompletableFuture 不指定线程池 │ │ │ │ ② parallelStream 并行流 │ │ │ │ ③ ForkJoinTask │ │ │ │ ④ 框架内部(Spring Data等) │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────┐ │ │ │ 你的自定义线程池(async等) │ │ │ └──────────────────────────────────┘ │ └──────────────────────────────────────────┘
为什么一个任务慢会卡住其他的?
因为公共池的线程是共享的,只有3-7个,一个任务占住线程不释放,其他任务就只能等。
用4核CPU举例(公共池只有3个线程): // 三个任务同时提交 CompletableFuture.supplyAsync(() -> callServiceA()); // 占线程1 CompletableFuture.supplyAsync(() -> callServiceB()); // 占线程2 CompletableFuture.supplyAsync(() -> callServiceC()); // 占线程3 // 现在第4个任务来了 CompletableFuture.supplyAsync(() -> callServiceD()); // 没有空闲线程了! // → callServiceD 必须等 callServiceA/B/C 有一个执行完才轮到它如果callServiceA卡住了:
线程1:callServiceA(远程接口超时,等了10秒还没返回) 线程2:callServiceB(正常,2秒完成) 线程3:callServiceC(正常,2秒完成) → 线程2、3完成后空闲 → callServiceD 占到线程2,开始执行 看起来还行?但如果同时来5个任务呢: 线程1:callServiceA(卡住10秒) 线程2:callServiceB(2秒完成)→ callServiceE(2秒完成)→ callServiceF(等线程1) 线程3:callServiceC(2秒完成)→ callServiceG(2秒完成)→ callServiceH(等线程1) 线程1释放后 → callServiceD终于开始执行 callServiceD等了8秒才开始!原本只需要2秒
而且更严重的情况:parallelStream也用公共池
// 业务代码1 CompletableFuture.supplyAsync(() -> callServiceA()); // 占线程1 // 业务代码2(完全不相关的模块) list.parallelStream() .map(item -> queryDB(item)) // 也要占线程 .collect(Collectors.toList()); // 两个模块互相抢线程,性能互相拖累实际项目中的隐患
公共池3个线程被占满的场景: 1. 三个CompletableFuture在调远程接口,其中一个超时卡了10秒 → 第4个CompletableFuture排队等10秒 → 响应时间从200ms飙到10秒 2. parallelStream在做大数据查询,占了2个线程 → 只剩1个线程给CompletableFuture用 → 所有异步任务都变慢 3. 框架内部也用公共池(比如Spring Data的一些异步操作) → 和你的业务代码抢线程
所以为什么不要用公共池
公共池的问题: ① 线程数太少(CPU核心数-1) ② 全局共享,所有任务互相抢线程 ③ 一个任务卡住影响其他任务 ④ 无法单独调优(不能改线程数、队列大小) 自定义线程池的好处: ① 线程数按场景设置 ② 业务隔离,互不影响 ③ 可以单独监控和调优 ④ 拒绝策略可以按场景选择
总结
ThreadPoolTaskExecutor是Spring对JDK原生ThreadPoolExecutor的封装。内部还是ThreadPoolExecutor,但额外提供了生命周期管理、线程名前缀、任务装饰器、@Async集成等功能。Spring项目推荐用ThreadPoolTaskExecutor,通用Java项目用ThreadPoolExecutor。
公共池是JVM的一个全局共享线程池,当你使用CompletableFuture不指定线程池或者用parallelStream时会自动使用。它的线程数默认只有CPU核心数-1,比如4核只有3个线程。
一个任务慢会卡住其他的,因为线程是共享的。比如三个任务同时用公共池,其中一个调远程接口超时卡了10秒,这10秒内那3个线程都被占着,其他异步任务只能排队等。所以实际项目中我不会用公共池,而是按业务场景创建独立的线程池,互不影响。
