← 返回题目列表

Executors 提供了哪几种线程池?为什么不推荐用它创建线程池?

高频 中等 第 10 / 31 题 更新于 2026/07/26
线程池ExecutorsThreadPoolExecutorOOM

简化版

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_VALUESynchronousQueue线程暴涨 → OOM/资源耗尽
newScheduledThreadPool(n)n / Integer.MAX_VALUEDelayedWorkQueue线程暴涨

它们本质都是对 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 死于无界线程,手动构造让队列有界、线程有上限、拒绝有策略」。