1. 概述

Java 并发编程中,线程池是提升性能、控制资源的核心组件。java.util.concurrent.Executors 工具类提供了多种快捷创建线程池的方式,其中最常用的两个是 newCachedThreadPool()newFixedThreadPool()

虽然它们用起来都很简单,但底层机制和适用场景却大不相同。本文将深入剖析这两个线程池的实现原理,对比其行为差异,并指出实际开发中的踩坑点,帮助你在高并发场景下做出更合理的选择。

2. Cached Thread Pool 剖析

我们先来看 Executors.newCachedThreadPool() 的源码实现(基于 OpenJDK):

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, 
      new SynchronousQueue<Runnable>());
}

关键参数解析:

  • 核心线程数(corePoolSize): 0
  • 最大线程数(maximumPoolSize): Integer.MAX_VALUE(理论上无限)
  • 空闲线程存活时间(keepAliveTime): 60 秒
  • 任务队列: SynchronousQueue

2.1 SynchronousQueue 的“反直觉”机制

SynchronousQueue 是一个不存储元素的阻塞队列。它的设计哲学是“同步交接(synchronous handoff)”——生产者线程必须等待一个消费者线程来直接接收任务,不能把任务“扔”进队列就走人。

这意味着:

队列永远是满的(因为不能存任务)
无法缓冲任务

当提交一个新任务时:

  1. 如果有空闲线程正在等待任务,则直接将任务交给它执行(手递手交接)。
  2. 如果没有空闲线程,由于队列无法排队,线程池会立即创建一个新线程来处理该任务。

2.2 动态伸缩与资源风险

  • 线程池初始为 0 个线程。
  • 随着任务涌入,线程数可动态增长至 Integer.MAX_VALUE(实际受限于系统资源)。
  • 空闲超过 60 秒的线程会被自动回收。

2.3 适用场景 ✅

适用于大量短生命周期、计算密集型的任务。

例如:

  • 批量处理轻量级计算任务(如 JSON 解析、简单数据转换)
  • 内部服务间快速响应的同步调用

2.4 踩坑场景 ❌

绝对不要用于 IO 密集型或执行时间不可控的任务!

来看一个反例:提交 100 万个微任务(每个耗时约 100 微秒)

Callable<String> task = () -> {
    long oneHundredMicroSeconds = 100_000;
    long startedAt = System.nanoTime();
    while (System.nanoTime() - startedAt <= oneHundredMicroSeconds);
    return "Done";
};

var cachedPool = Executors.newCachedThreadPool();
var tasks = IntStream.rangeClosed(1, 1_000_000)
    .mapToObj(i -> task)
    .collect(Collectors.toList());
var result = cachedPool.invokeAll(tasks);

后果很严重:

⚠️ 疯狂创建线程:可能瞬间创建数万个线程
⚠️ 内存爆炸:每个线程默认栈大小 1MB,1 万个线程就是 10GB 内存
⚠️ CPU 上下文切换风暴:频繁的线程调度导致 CPU 利用率虚高,实际吞吐下降

结论:IO 型任务(如数据库查询、HTTP 调用)绝不能用 cached pool!

3. Fixed Thread Pool 剖析

再来看 newFixedThreadPool(int nThreads) 的实现:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, 
      new LinkedBlockingQueue<Runnable>());
}

关键参数解析:

  • 核心线程数 = 最大线程数: nThreads(固定大小)
  • 空闲存活时间: 0(核心线程永不回收)
  • 任务队列: LinkedBlockingQueue(无界队列)

3.1 工作机制

  • 线程池启动时就创建 nThreads 个线程,数量固定不变
  • 提交任务时:
    • 如果有空闲线程,直接执行。
    • 如果所有线程都在忙,任务会被放入 LinkedBlockingQueue 中等待。
  • 由于队列是无界的(默认容量为 Integer.MAX_VALUE),任务可以无限堆积。

3.2 优势 ✅

  • 资源可控:线程数固定,避免线程爆炸。
  • 适合不可预测耗时任务:通过队列缓冲,防止瞬时高峰压垮系统。

3.3 典型适用场景 ✅

  • 处理 HTTP 请求(如 Tomcat 默认线程池)
  • 异步日志写入
  • 第三方 API 调用(网络延迟不可控)

4. 共同的“致命”缺陷:资源失控 ⚠️

你可能注意到,newCachedThreadPool()newFixedThreadPool() 都使用了 AbortPolicy 作为拒绝策略——理论上任务过多时应抛出 RejectedExecutionException

但现实是:它们几乎永远不会触发拒绝!

线程池类型 饱和条件 实际行为
cachedPool 线程数达到 MAX_VALUE 几乎不可能,先 OOM
fixedPool 队列满且线程全忙 队列无界 → 永远不会满 → 无限堆积

4.1 真实风险

在高负载下:

  • Fixed Pool: 任务无限堆积 → 堆内存溢出(OOM)
  • Cached Pool: 线程无限创建 → 线程栈耗尽 + CPU 上下文切换严重

两者都会导致服务雪崩。

4.2 正确做法:自定义有界线程池 ✅

要真正控制资源,必须使用 ThreadPoolExecutor 手动配置:

// 有界队列 + 有限最大线程数 + 拒绝策略
var boundedQueue = new ArrayBlockingQueue<Runnable>(1000);
var executor = new ThreadPoolExecutor(
    10,           // 核心线程数
    20,           // 最大线程数
    60, TimeUnit.SECONDS,
    boundedQueue, // 有界队列,最多 1000 个任务
    new ThreadPoolExecutor.AbortPolicy() // 队列满时抛出异常
);

这样配置后:

  • 最多 20 个线程
  • 最多排队 1000 个任务
  • 超出负载直接拒绝,保护系统稳定性

5. 总结

对比项 newCachedThreadPool() newFixedThreadPool()
适用任务类型 短生命周期、CPU 密集型 长时间、IO 密集型、耗时不确定
线程数 动态,最多 MAX_VALUE 固定
任务队列 SynchronousQueue(无缓冲) LinkedBlockingQueue(无界)
资源风险 线程爆炸、CPU 切换 任务堆积、内存溢出
是否推荐直接使用 ❌ 不推荐 ❌ 不推荐
推荐替代方案 自定义 ThreadPoolExecutor 自定义 ThreadPoolExecutor

最终建议

生产环境永远不要直接使用 Executors 工厂方法创建线程池!
✅ 必须根据业务场景,手动创建有界队列 + 明确拒绝策略ThreadPoolExecutor
✅ 合理设置核心线程数、最大线程数、队列容量,并做好监控和降级。

这才是高并发系统的正确打开方式


原始标题:Executors newCachedThreadPool() vs newFixedThreadPool()