线程池是Java并发编程中最常用的工具之一,但《阿里巴巴Java开发手册》明确禁止使用Executors创建线程池,必须通过ThreadPoolExecutor的方式手动创建。本文带你彻底搞懂线程池的核心原理。
一、为什么要用线程池
1.1 线程创建的开销
每次 new Thread 都会涉及:
- 内存分配:每个线程栈默认占用 1MB 内存
- 系统调用:需要操作系统内核创建线程
- 上下文切换:线程调度带来的 CPU 开销
- 销毁成本:线程执行完后需要 GC 回收
如果频繁创建销毁线程,性能损耗非常大。
1.2 线程池的优势
- 降低资源消耗:复用已创建的线程,减少创建销毁开销
- 提高响应速度:任务到达时无需等待线程创建
- 提高线程可管理性:统一分配、调优和监控
- 附加功能:提供定时、延迟、并发控制等增强功能
二、ThreadPoolExecutor 七大核心参数
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 线程工厂
- 用于创建线程的工厂类
- 可以自定义线程名称、是否守护线程、优先级等
- 生产环境建议自定义,方便排查问题时识别线程归属
ThreadFactory factory = new ThreadFactoryBuilder()
.setNameFormat("my-pool-%d")
.setDaemon(false)
.build();
⑦ handler 拒绝策略
当线程池和队列都满了,新任务的处理策略:
| 策略 | 行为 |
|---|---|
AbortPolicy |
默认,直接抛出 RejectedExecutionException 异常 |
CallerRunsPolicy |
由提交任务的线程自己执行(降级) |
DiscardPolicy |
直接丢弃任务,不抛异常 |
DiscardOldestPolicy |
丢弃队列中最老的任务,重试提交 |
三、线程池执行流程
记住这个执行顺序,面试高频考点!
任务提交后的完整判断流程:
新任务提交
↓
当前线程数 < corePoolSize?
├─ 是 → 创建核心线程执行任务 ✅
└─ 否 → 队列是否已满?
├─ 否 → 任务加入队列排队 ✅
└─ 是 → 当前线程数 < maxPoolSize?
├─ 是 → 创建非核心线程执行 ✅
└─ 否 → 执行拒绝策略 ❌
流程图文字版
- 第一步:判断核心线程是否都在工作。没满就新建核心线程执行
- 第二步:核心线程满了,尝试放入阻塞队列排队
- 第三步:队列也满了,判断是否达到最大线程数。没到就创建临时线程
- 第四步:都满了,触发拒绝策略
四、Executors 提供的四种线程池
4.1 newFixedThreadPool — 固定大小线程池
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 — 缓存线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()
);
}
- 核心线程 0,最大线程无界
- 线程空闲 60 秒自动回收
- 风险:并发高时可能创建大量线程导致 OOM
4.3 newSingleThreadExecutor — 单线程池
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 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
原因:
FixedThreadPool和SingleThreadPool→ 队列长度 Integer.MAX_VALUE,可能堆积 OOMCachedThreadPool和ScheduledThreadPool→ 最大线程数 Integer.MAX_VALUE,可能创建过多线程 OOM
5.2 正确创建方式(推荐)
// 使用 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 监控指标
// 获取线程池状态
executor.getActiveCount(); // 活跃线程数
executor.getPoolSize(); // 当前线程总数
executor.getCorePoolSize(); // 核心线程数
executor.getLargestPoolSize(); // 历史最大线程数
executor.getTaskCount(); // 总任务数
executor.getCompletedTaskCount(); // 已完成任务数
executor.getQueue().size(); // 队列排队任务数
6.2 关闭线程池
// 平缓关闭:不再接收新任务,等待已提交任务执行完
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:
@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,合理设置七大参数,并做好监控,才能发挥线程池的真正价值。