Executors 提供了哪几种线程池?为什么不推荐用它创建线程池?
简化版
Executors 是创建线程池的快捷工厂,常见四种:newFixedThreadPool(固定大小)、newCachedThreadPool(可无限扩张)、newSingleThreadExecutor(单线程)、newScheduledThreadPool(定时/周期任务)。不推荐用它们的原因:FixedThreadPool 和 SingleThreadExecutor 用无界队列(LinkedBlockingQueue,容量 Integer.MAX_VALUE),任务会无限堆积导致 OOM;CachedThreadPool 和 ScheduledThreadPool 的最大线程数是 Integer.MAX_VALUE,可能创建海量线程耗尽资源。阿里规范要求手动 new ThreadPoolExecutor,显式指定有界队列和合理的线程数,让风险可控。
详细版
四种线程池及其隐患:
| 工厂方法 | 核心/最大线程 | 队列 | 隐患 |
|---|---|---|---|
newFixedThreadPool(n) | n / n | 无界 LinkedBlockingQueue | 任务堆积 → OOM |
newSingleThreadExecutor() | 1 / 1 | 无界 LinkedBlockingQueue | 任务堆积 → OOM |
newCachedThreadPool() | 0 / Integer.MAX_VALUE | SynchronousQueue | 线程暴涨 → OOM/资源耗尽 |
newScheduledThreadPool(n) | n / Integer.MAX_VALUE | DelayedWorkQueue | 线程暴涨 |
它们本质都是对 ThreadPoolExecutor 的封装,只是参数配得「危险」:要么队列无界、要么线程数无界。
推荐做法:手动构造,七个参数全掌控:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, // corePoolSize 核心线程数
16, // maximumPoolSize 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(1000), // 有界队列!关键
new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(), // 命名,便于排查
new ThreadPoolExecutor.CallerRunsPolicy() // 明确的拒绝策略
);
⚠️ 无界队列的危害最隐蔽:
newFixedThreadPool(10)看着「只有 10 个线程很安全」,但队列能存 21 亿个任务。流量突增时任务全堆进队列,内存被撑爆 OOM——而且maximumPoolSize因为队列永远不满,根本用不上。
完整版教学
一、先理解线程池的通用引擎:ThreadPoolExecutor
四种 Executors 线程池都是同一个引擎 ThreadPoolExecutor 的不同配置。它的任务处理逻辑是固定的:
提交任务时:
1. 线程数 < corePoolSize → 新建核心线程执行
2. 核心线程满 → 任务进队列排队
3. 队列也满 且 线程数 < maxPoolSize → 新建非核心线程执行
4. 队列满 且 线程数 = maxPoolSize → 触发拒绝策略
理解这个流程,就能看穿每种 Executors 的「危险点」到底在哪一步失控:无界队列让第 2 步永不溢出(第 3、4 步永不触发,任务无限堆积);无界 maxPoolSize 让第 3 步无限建线程。所有隐患都是「某个边界被设成了无穷大」。
二、FixedThreadPool / SingleThreadExecutor:无界队列的 OOM 陷阱
这两个用无界队列 LinkedBlockingQueue(默认容量 Integer.MAX_VALUE):
newFixedThreadPool(10) 实际参数:
core=10, max=10, queue=LinkedBlockingQueue() // 容量 21 亿
流量正常:10 个线程够用,队列空
流量突增:任务提交速度 > 处理速度
→ 队列开始堆积:1万、10万、100万...
→ 每个任务对象占内存,堆积到一定量 → OutOfMemoryError
→ 且因为队列"永远不满",max=10 这个上限毫无意义
用一个数字感受:假设每个任务对象加其引用的数据约占 1KB,堆内存 2GB,那么堆积约 200 万个任务就 OOM。高峰期几分钟就能积压到这个量级。SingleThreadExecutor 同理,只是核心线程数为 1,堆积更快。「固定大小」给人安全的错觉,真正的炸弹在队列。
三、CachedThreadPool / ScheduledThreadPool:无界线程数
这两个把最大线程数设成 Integer.MAX_VALUE:
newCachedThreadPool() 实际参数:
core=0, max=Integer.MAX_VALUE, queue=SynchronousQueue(容量0,不缓冲)
SynchronousQueue 不存任务,来一个任务就要求立刻有线程接:
没有空闲线程 → 新建线程
流量突增:每个任务都新建线程
→ 线程数飙升:1千、1万...
→ 每个线程默认占约 1MB 栈内存 + 内核调度开销
→ 创建几千个线程就可能 OOM: unable to create native thread / 系统卡死
一个数字:1 万个线程 ≈ 10GB 栈内存(每线程默认 1MB 栈),远超普通机器承受。CachedThreadPool 适合「短平快、量不大」的任务,一旦有慢任务 + 高并发,线程会失控暴涨。ScheduledThreadPool 的 max 也是无穷大,同理有风险。
四、为什么手动 new ThreadPoolExecutor 更安全
阿里《Java 开发手册》明确规定:线程池不允许用 Executors 创建,必须用 ThreadPoolExecutor。手动构造能把每个「无穷大」都换成可控值:
| 风险点 | Executors | 手动构造 |
|---|---|---|
| 队列容量 | 无界(可能堆积 OOM) | ArrayBlockingQueue(1000) 有界 |
| 最大线程数 | 可能 Integer.MAX_VALUE | 按机器核数设合理上限 |
| 队列满/线程满 | 悄悄堆积或暴涨 | 触发明确的拒绝策略 |
| 线程命名 | 默认 pool-N-thread-M,排查难 | 自定义 ThreadFactory 命名 |
核心价值是让系统在过载时「优雅拒绝」而不是「静默崩溃」:有界队列 + 合理 max + 拒绝策略,过载时能快速失败、降级、记日志、告警,而不是把内存或线程数吃到 OOM 才发现。命名线程则让线上 dump 分析时一眼看出是哪个业务池出问题。
五、四种池对应的手动写法
如果就想要类似 Fixed/Cached 的行为,可以手动构造安全版:
// 类 FixedThreadPool,但有界队列 + 拒绝策略
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(500),
new ThreadPoolExecutor.CallerRunsPolicy());
// 类 CachedThreadPool,但限制最大线程数
new ThreadPoolExecutor(0, 200, 60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadPoolExecutor.AbortPolicy());
// 定时任务:Spring 环境可用 ThreadPoolTaskScheduler,或第三方 xxl-job
原则不变:队列必须有界、最大线程数必须有上限、拒绝策略必须明确。三者共同构成「过载保护」。
六、线程池参数怎么定(起点公式)
手动构造要自己定线程数,给一个起点公式:
CPU 密集型(计算为主):线程数 ≈ CPU 核数 + 1
(多一个防止偶发缺页/中断时 CPU 空转)
IO 密集型(等待为主):线程数 ≈ CPU 核数 × (1 + 平均等待时间/平均计算时间)
例:8 核,每个任务 90% 时间在等 IO(等9ms算1ms)
→ 8 × (1 + 9/1) = 80 个线程
公式只是起点,真实值要压测调优——观察 CPU 利用率、队列长度、响应时间。队列容量则要从「流量峰值 × 可容忍延迟」反推,不能拍脑袋设个 Integer.MAX_VALUE 了事。
记忆钩子:「Fixed/Single 死在无界队列(OOM),Cached/Scheduled 死在无界线程数(资源耗尽);手动 new,队列有界、线程有上限、拒绝要明确」。
七、常见误区与追问
- 误区:newFixedThreadPool(10) 只有 10 个线程所以很安全。 线程数是固定的,但队列无界,任务会无限堆积到 OOM,maximumPoolSize 还形同虚设。
- 误区:CachedThreadPool 会自动回收线程所以不会有问题。 它的最大线程数是 Integer.MAX_VALUE,高并发下线程会暴涨到耗尽资源,回收赶不上创建。
- 误区:Executors 创建的池不能用。 能用,只是在流量不可控的生产环境有 OOM 风险;小工具、可控场景用也无妨,但规范建议手动构造。
- 误区:手动构造只是为了自定义线程数。 更重要的是把无界队列换成有界、把无穷大 max 换成上限、加明确拒绝策略和线程命名,实现过载保护和可排查。
- 追问:为什么无界队列会让 maximumPoolSize 失效? 线程池只在「队列满」时才新建非核心线程;无界队列永远不满,所以永远不会创建超过 core 的线程,max 参数没有意义。
- 追问:SingleThreadExecutor 和 newFixedThreadPool(1) 有什么区别? 前者用 FinalizableDelegatedExecutorService 包装,不能被强转后修改线程数;后者是裸 ThreadPoolExecutor,可被强转后调 setCorePoolSize 改大小。
- 追问:定时任务除了 ScheduledThreadPool 还有什么选择? Spring 的 ThreadPoolTaskScheduler、分布式场景用 xxl-job/ElasticJob,避免单机 ScheduledThreadPool 的无界线程和单点问题。
八、加强记忆
Executors 四种线程池本质都是 ThreadPoolExecutor 的封装,隐患都源于「某个边界被设成无穷大」:FixedThreadPool 和 SingleThreadExecutor 用无界 LinkedBlockingQueue,任务无限堆积撑爆内存(OOM),且队列永不满导致 maximumPoolSize 失效;CachedThreadPool 和 ScheduledThreadPool 把最大线程数设成 Integer.MAX_VALUE,高并发下线程暴涨耗尽资源。所以阿里规范要求手动 new ThreadPoolExecutor——把队列换成有界(ArrayBlockingQueue)、把 max 设成合理上限、配明确的拒绝策略、给线程命名,让系统过载时「优雅拒绝」而非「静默崩溃」。线程数按 CPU 密集(核数+1)/ IO 密集(核数×(1+等待/计算))估起点再压测调优。记住一句「Fixed/Single 死于无界队列、Cached/Scheduled 死于无界线程,手动构造让队列有界、线程有上限、拒绝有策略」。