线程池有几种创建方式?各自适合什么场景?
结论先行:线程池的创建入口有四种——Executors 工厂方法、ThreadPoolExecutor 构造器、Spring 的 ThreadPoolTaskExecutor、JDK 21 的虚拟线程执行器。它们最终都落到同一个 ThreadPoolExecutor 核心,区别只在谁能把参数说清楚。
1. 四种入口
| 入口 | 一句话 | 风险 |
|---|---|---|
Executors.newXxx() | 一行搞定,参数被隐藏 | 无界队列或无界线程数,生产禁用 |
new ThreadPoolExecutor(...) | 七个参数全部显式 | 长,但可控、可监控 |
ThreadPoolTaskExecutor | Spring 封装,便于 @Async 注入 | 默认 queueCapacity=Integer.MAX_VALUE,同样是坑 |
Executors.newVirtualThreadPerTaskExecutor() | 一任务一虚拟线程 | 只适合 IO 密集,且要限制下游并发 |
2. 家族关系
Executor(接口,只有 execute 一个方法)
└── ExecutorService(接口:submit / invokeAll / shutdown)
├── ThreadPoolExecutor 最常用,七个参数全暴露
│ └── ScheduledThreadPoolExecutor 定时/周期任务
├── ForkJoinPool 工作窃取,commonPool 也是它
└── 以上实例都可以由 Executors 工厂方法产出
└── Spring 再包一层:ThreadPoolTaskExecutor
3. 三种推荐写法
// 写法一:直接构造(最可控,推荐)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, // corePoolSize
16, // maximumPoolSize
60, TimeUnit.SECONDS, // 空闲回收时间
new ArrayBlockingQueue<>(500), // 有界队列
new ThreadFactory() {
private final AtomicInteger seq = new AtomicInteger(1);
@Override public Thread newThread(Runnable r) {
Thread t = new Thread(r, "order-pool-" + seq.getAndIncrement());
t.setDaemon(false);
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy());
// 写法二:Spring 环境(配合 @Async 使用)
@Bean("orderExecutor")
public ThreadPoolTaskExecutor orderExecutor() {
ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(8);
ex.setMaxPoolSize(16);
ex.setQueueCapacity(500); // 必须显式设置,否则无界
ex.setThreadNamePrefix("order-");
ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
ex.setWaitForTasksToCompleteOnShutdown(true);
ex.setAwaitTerminationSeconds(30);
ex.initialize();
return ex;
}
// 写法三:JDK 21 虚拟线程(IO 密集、任务量大)
try (ExecutorService vt = Executors.newVirtualThreadPerTaskExecutor()) {
for (Request req : requests) {
vt.submit(() -> handle(req));
}
}
4. 场景对照
| 场景 | 选择 | 关键参数 |
|---|---|---|
| 核心交易链路 | ThreadPoolExecutor | 小 core、有界队列、CallerRunsPolicy |
| 旁路任务(发短信、写日志) | 独立小池 | 队列可小,拒绝可丢弃 + 告警 |
| 定时任务 | ScheduledThreadPoolExecutor | 注意任务抛异常会终止后续调度 |
| 并行计算 | ForkJoinPool | 分治任务,工作窃取 |
| 高并发 IO(网关、爬虫) | 虚拟线程执行器 | 配合 Semaphore 限流下游 |
| Spring 异步方法 | ThreadPoolTaskExecutor | 必须显式 queueCapacity |
5. 常见追问
追问一:ThreadPoolTaskExecutor 和 ThreadPoolExecutor 是什么关系?
前者是 Spring 的包装类,实现 AsyncTaskExecutor,内部持有一个 ThreadPoolExecutor。它的默认值很危险:corePoolSize=1、maxPoolSize=Integer.MAX_VALUE、queueCapacity=Integer.MAX_VALUE——不显式配置就等于在用一个无界池。
追问二:为什么不推荐 Executors?
它把"队列容量"和"最大线程数"这两个决定生死的参数藏起来了:newFixedThreadPool 用无界队列,newCachedThreadPool 用无界线程数。详见"为什么禁止使用 Executors"一篇。
追问三:new Thread() 算创建线程池吗?
不算。线程池的核心价值是复用 + 排队 + 拒绝 + 监控,裸线程每次都要走 OS 创建,量大时直接把机器拖垮。
追问四:ScheduledThreadPoolExecutor 有什么坑?
任务里抛出的未捕获异常会终止该任务的后续调度(不会影响其他任务,但周期任务悄悄停了)。所以定时任务体必须整体 try-catch,否则"任务只跑了一次"这种 bug 很难发现。
小结
创建线程池的选择只有一句话:能用构造器就别用工厂,能显式填队列容量就别用默认值。Executors 适合写 demo,ThreadPoolExecutor 适合上生产,虚拟线程执行器适合 IO 密集的新代码。