为什么禁止使用 Executors 默认工厂创建线程池?
结论先行:Executors 的静态工厂把关键参数藏起来了,容易埋雷:newFixedThreadPool / newSingleThreadExecutor 使用无界 LinkedBlockingQueue,newCachedThreadPool 的最大线程数是 Integer.MAX_VALUE。前者让任务无限堆积直至内存耗尽,后者在流量洪峰时无限创建线程直至系统资源耗尽,因此规范要求手写 ThreadPoolExecutor 显式定参。
自检原则:队列必须有界、线程必须有上限、拒绝必须有策略,三者缺一不可,Executors 默认工厂恰好都做不到。
典型坑位
| Executors 方法 | 队列 / 线程配置 | 隐患 |
|---|---|---|
| newFixedThreadPool | 无界队列 | 任务积压到 OOM,且线程数固定为核心数 |
| newSingleThreadExecutor | 无界队列 | 同上,单线程积压更明显 |
| newCachedThreadPool | SynchronousQueue + 最大线程无限 | 洪峰时线程暴涨,句柄/内存耗尽 |
| newScheduledThreadPool | 无界延迟队列 | 周期任务堆积,问题难排查 |
- 无界队列意味着“永远不会拒绝”,背压机制失效,故障最终以内存溢出的形式爆发。
- 默认 threadFactory 生成的线程名形如 pool-1-thread-1,出问题时难以定位线程归属与来源。
- 故障画像:无界队列的故障是内存缓慢爬升后 OOM;无界线程的故障是线程数/句柄数瞬间打满,且难以恢复。
- 背压价值:手写线程池的有界队列 + 拒绝策略会把过载信号及时反馈给提交方。
import java.util.concurrent.*;
public class ExecutorsPitfall {
public static void main(String[] args) {
// 反例:无界队列 + 固定线程,任务多时全部积压
ExecutorService bad = Executors.newFixedThreadPool(4);
// 正确姿势:有界队列 + 自定义命名工厂 + 明确拒绝策略
ThreadPoolExecutor good = new ThreadPoolExecutor(
4, 8, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
r -> new Thread(r, "biz-pool-" + r.hashCode()),
new ThreadPoolExecutor.CallerRunsPolicy());
}
}
常见追问 / 记忆点
- 记忆点:Executors 两大坑 = 无界队列(积压 OOM)+ 无界线程(暴涨失控),手写参数才可控。
- 追问:业界规约明确禁止 Executors,核心原因是它无法预估任务积压量与线程创建量。
- 追问:参数估算经验:CPU 密集型 ≈ 核数 + 1;IO 密集型可放宽到核数 × 2 或按“等待时间/计算时间”比例放大。