Java线程池ThreadPoolExecutor深度解析

线程池是Java并发编程中最常用的工具之一,但《阿里巴巴Java开发手册》明确禁止使用Executors创建线程池,必须通过ThreadPoolExecutor的方式手动创建。本文带你彻底搞懂线程池的核心原理。

一、为什么要用线程池

1.1 线程创建的开销

每次 new Thread 都会涉及:

  1. 内存分配:每个线程栈默认占用 1MB 内存
  2. 系统调用:需要操作系统内核创建线程
  3. 上下文切换:线程调度带来的 CPU 开销
  4. 销毁成本:线程执行完后需要 GC 回收

如果频繁创建销毁线程,性能损耗非常大。

1.2 线程池的优势

  • 降低资源消耗:复用已创建的线程,减少创建销毁开销
  • 提高响应速度:任务到达时无需等待线程创建
  • 提高线程可管理性:统一分配、调优和监控
  • 附加功能:提供定时、延迟、并发控制等增强功能

二、ThreadPoolExecutor 七大核心参数

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 存活时间单位
BlockingQueue<Runnable> workQueue, // 5. 工作队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
)

2.1 参数详解

① corePoolSize 核心线程数

  • 线程池中常驻的核心线程数量
  • 即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut=true
  • 提交任务时,如果当前线程数 < corePoolSize,直接创建新线程执行

② maximumPoolSize 最大线程数

  • 线程池允许创建的最大线程数量
  • 当工作队列满了,且当前线程数 < maximumPoolSize 时,会创建非核心线程
  • 必须 >= corePoolSize

③ keepAliveTime 空闲存活时间

  • 非核心线程空闲后的最大存活时间
  • 超过时间还没有新任务,该线程会被销毁
  • 核心线程默认不超时,可通过 allowCoreThreadTimeOut(true) 开启

④ unit 时间单位

  • TimeUnit.SECONDS / TimeUnit.MILLISECONDS

⑤ workQueue 工作队列

任务排队的阻塞队列,常用实现:

队列类型 特点 适用场景
ArrayBlockingQueue 有界数组队列,FIFO 需要控制队列大小
LinkedBlockingQueue 链表队列(默认Integer.MAX) 固定核心线程数场景
SynchronousQueue 不存储任务,直接交给线程 配合无界最大线程数
PriorityBlockingQueue 优先级无界队列 任务有优先级要求
DelayedWorkQueue 延迟队列 定时任务场景

⑥ threadFactory 线程工厂

  • 用于创建线程的工厂类
  • 可以自定义线程名称、是否守护线程、优先级等
  • 生产环境建议自定义,方便排查问题时识别线程归属
1
2
3
4
ThreadFactory factory = new ThreadFactoryBuilder()
.setNameFormat("my-pool-%d")
.setDaemon(false)
.build();

⑦ handler 拒绝策略

当线程池和队列都满了,新任务的处理策略:

策略 行为
AbortPolicy 默认,直接抛出 RejectedExecutionException 异常
CallerRunsPolicy 由提交任务的线程自己执行(降级)
DiscardPolicy 直接丢弃任务,不抛异常
DiscardOldestPolicy 丢弃队列中最老的任务,重试提交

三、线程池执行流程

记住这个执行顺序,面试高频考点!

任务提交后的完整判断流程:

1
2
3
4
5
6
7
8
9
新任务提交

当前线程数 < corePoolSize?
├─ 是 → 创建核心线程执行任务 ✅
└─ 否 → 队列是否已满?
├─ 否 → 任务加入队列排队 ✅
└─ 是 → 当前线程数 < maxPoolSize?
├─ 是 → 创建非核心线程执行 ✅
└─ 否 → 执行拒绝策略 ❌

流程图文字版

  1. 第一步:判断核心线程是否都在工作。没满就新建核心线程执行
  2. 第二步:核心线程满了,尝试放入阻塞队列排队
  3. 第三步:队列也满了,判断是否达到最大线程数。没到就创建临时线程
  4. 第四步:都满了,触发拒绝策略

四、Executors 提供的四种线程池

4.1 newFixedThreadPool — 固定大小线程池

1
2
3
4
5
6
7
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
);
}
  • 核心线程 = 最大线程,都是 nThreads
  • 队列是无界的(Integer.MAX_VALUE)
  • 风险:队列可能堆积大量任务导致 OOM

4.2 newCachedThreadPool — 缓存线程池

1
2
3
4
5
6
7
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()
);
}
  • 核心线程 0,最大线程无界
  • 线程空闲 60 秒自动回收
  • 风险:并发高时可能创建大量线程导致 OOM

4.3 newSingleThreadExecutor — 单线程池

1
2
3
4
5
6
7
8
9
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(
1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
)
);
}
  • 只有一个工作线程,保证任务顺序执行
  • 风险:队列无界,可能 OOM

4.4 newScheduledThreadPool — 定时任务线程池

  • 核心线程数固定,最大线程无界
  • 使用 DelayedWorkQueue 延迟队列
  • 支持定时和周期性任务执行

五、生产环境正确配置

5.1 为什么禁止用 Executors

《阿里巴巴Java开发手册》强制规定:

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

原因:

  • FixedThreadPoolSingleThreadPool → 队列长度 Integer.MAX_VALUE,可能堆积 OOM
  • CachedThreadPoolScheduledThreadPool → 最大线程数 Integer.MAX_VALUE,可能创建过多线程 OOM

5.2 正确创建方式(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用 Guava 的 ThreadFactoryBuilder
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("order-pool-%d")
.setUncaughtExceptionHandler((t, e) ->
log.error("线程{}发生异常", t.getName(), e)
)
.build();

ThreadPoolExecutor executor = new ThreadPoolExecutor(
8, // 核心线程数
16, // 最大线程数
60, // 空闲存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(100), // 有界队列
threadFactory, // 自定义线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用方执行
);

5.3 线程数设置经验

CPU 密集型任务

  • 特点:大量计算,CPU 使用率高
  • 推荐:corePoolSize = CPU核心数 + 1
  • 例如 8 核 CPU → 设为 9

IO 密集型任务

  • 特点:数据库查询、网络调用等,线程经常阻塞
  • 推荐:corePoolSize = CPU核心数 * 2 或更高
  • 公式:线程数 = CPU核数 * (1 + 等待时间/计算时间)

混合型任务

  • 根据业务拆分,不同类型任务使用不同线程池
  • 不要一个线程池处理所有任务

六、线程池监控与常用方法

6.1 监控指标

1
2
3
4
5
6
7
8
// 获取线程池状态
executor.getActiveCount(); // 活跃线程数
executor.getPoolSize(); // 当前线程总数
executor.getCorePoolSize(); // 核心线程数
executor.getLargestPoolSize(); // 历史最大线程数
executor.getTaskCount(); // 总任务数
executor.getCompletedTaskCount(); // 已完成任务数
executor.getQueue().size(); // 队列排队任务数

6.2 关闭线程池

1
2
3
4
5
6
7
8
9
10
11
12
// 平缓关闭:不再接收新任务,等待已提交任务执行完
executor.shutdown();

// 立即关闭:尝试中断正在执行的任务,返回等待中的任务列表
List<Runnable> tasks = executor.shutdownNow();

// 判断是否终止
executor.isShutdown();
executor.isTerminated();

// 等待终止,带超时
executor.awaitTermination(1, TimeUnit.HOURS);

七、常见问题与避坑

7.1 任务异常丢失

线程池中的任务如果抛出异常且没有捕获,线程会销毁重建,异常堆栈可能丢失。

  • 解决方案:使用 try-catch 包裹任务逻辑,或自定义 UncaughtExceptionHandler

7.2 队列任务堆积

  • 必须使用有界队列(ArrayBlockingQueue)
  • 配合合理的拒绝策略
  • 做好监控告警,队列长度超过阈值及时预警

7.3 线程池死锁

  • 同一个线程池中,任务之间互相等待结果可能导致死锁
  • 建议不同层级的任务使用不同的线程池

7.4 Spring 中使用线程池

推荐使用 Spring 封装的 ThreadPoolTaskExecutor

1
2
3
4
5
6
7
8
9
10
11
12
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}

总结:线程池的核心是”池化思想”,通过复用线程减少开销。生产环境务必手动创建 ThreadPoolExecutor,合理设置七大参数,并做好监控,才能发挥线程池的真正价值。