深入浅出Java线程池
什么是线程池?
想象你开了一家快递站,每天有很多包裹要派送。如果每来一个包裹就雇一个新快递员,送完就解雇,这样效率很低,因为:
- 频繁招聘和解雇成本高(对应线程创建和销毁开销大)
- 新快递员不熟悉路线(线程需要时间初始化)
- 快递员太多时管理混乱(系统资源耗尽)
线程池就像你预先雇佣的一批固定快递员(线程),有包裹(任务)来了就分配给他们,送完继续等待新任务,这样效率更高。
为什么需要线程池?
- 降低资源消耗:重复利用已创建的线程,避免频繁创建销毁
- 提高响应速度:任务到达时直接使用现有线程,无需等待线程创建
- 便于管理:可以统一分配、监控和调优线程资源
Java线程池核心类
Java中的线程池主要通过java.util.concurrent包中的ExecutorService接口及其实现类ThreadPoolExecutor来实现。
线程池工作原理
线程池就像一个有管理的"线程工厂+任务队列":
- 核心线程:池中常驻的基本劳动力,即使空闲也不销毁
- 任务队列:当核心线程都忙时,新任务进入队列等待
- 非核心线程:当队列满了,创建额外线程帮忙(有数量限制)
- 拒绝策略:当线程和队列都满了,如何处理新任务
创建线程池的常用方法
Java提供了Executors工厂类来创建常见类型的线程池:
// 1. 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 2. 单线程池(保证任务顺序执行)
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
// 3. 可缓存线程池(自动扩容缩容)
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 4. 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
更灵活的ThreadPoolExecutor
实际上,上述工厂方法内部都是使用ThreadPoolExecutor构造的。直接使用它可更精细控制:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, // 空闲线程存活时间(秒)
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(100), // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
线程池重要参数
- corePoolSize:核心线程数,池中常驻线程数量
- maximumPoolSize:最大线程数,池中允许的最大线程数
- keepAliveTime:非核心线程空闲时的存活时间
- workQueue:任务队列,保存等待执行的任务
- threadFactory:创建线程的工厂
- handler:拒绝策略,当线程和队列都满时的处理方式
四种拒绝策略
- AbortPolicy(默认):直接抛出RejectedExecutionException异常
- CallerRunsPolicy:让提交任务的线程自己执行该任务
- DiscardPolicy:默默丢弃无法处理的任务
- DiscardOldestPolicy:丢弃队列中最老的任务,然后重试提交
线程池生命周期
- RUNNING:接受新任务,处理队列任务
- SHUTDOWN:不接受新任务,但处理队列中的任务
- STOP:不接受新任务,不处理队列任务,中断正在执行的任务
- TIDYING:所有任务终止,workerCount为0
- TERMINATED:terminated()方法执行完毕
使用示例
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
// 提交10个任务
for (int i = 1; i <= 10; i++) {
final int taskId = i;
pool.execute(() -> {
System.out.println("任务" + taskId + "正在执行,线程:" + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务" + taskId + "执行完毕");
});
}
// 关闭线程池
pool.shutdown();
}
}
任务提交后的完整流程(逐层判断)
流程图解:
graph TD
A[提交新任务] --> B{核心线程是否空闲?}
B -->|是| C[交给空闲核心线程执行]
B -->|否| D{任务队列是否未满?}
D -->|是| E[任务入队等待]
D -->|否| F{当前线程数 < maximumPoolSize?}
F -->|是| G[创建非核心线程执行]
F -->|否| H[触发拒绝策略]
分步骤详解:
-
核心线程优先
- 如果当前运行的线程数
< corePoolSize(默认4个),立即创建新线程执行任务(即使其他核心线程空闲)。 - 注:可通过
allowCoreThreadTimeOut(true)让核心线程超时回收。
- 如果当前运行的线程数
-
任务队列缓冲
- 如果核心线程全忙,任务会被放入
workQueue(如LinkedBlockingQueue)。 - 经典坑点:如果使用无界队列(如未设置容量的
LinkedBlockingQueue),maximumPoolSize参数会失效,永远不会创建非核心线程。
- 如果核心线程全忙,任务会被放入
-
非核心线程应急
- 当队列已满且线程数
< maximumPoolSize(如8个),会创建临时线程处理新任务。 - 这些线程在空闲
keepAliveTime后会被回收(默认只回收非核心线程)。
- 当队列已满且线程数
-
拒绝策略兜底
- 当队列满且线程数达到
maximumPoolSize时,触发RejectedExecutionHandler。 - 内置4种策略:
AbortPolicy(默认):直接抛出RejectedExecutionExceptionCallerRunsPolicy:让提交任务的线程自己执行DiscardPolicy:静默丢弃任务DiscardOldestPolicy:丢弃队列中最旧的任务后重试
- 当队列满且线程数达到
关键参数组合的实战表现
场景1:核心线程4 + 最大线程8 + 有界队列(容量10)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, 8, 30, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10) // 有界队列
);
- 任务提交顺序:
- 前4个任务 → 立即创建4个核心线程执行
- 第5~14个任务 → 进入队列
- 第15~18个任务 → 创建4个非核心线程(总线程数=8)
- 第19个任务 → 触发拒绝策略
场景2:核心线程4 + 最大线程4 + 无界队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, 4, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>() // 无界队列
);
- 表现:
- 永远只有4个核心线程,新任务无限排队(
maximumPoolSize无效) - 风险:可能引发OOM(队列无限增长)
- 永远只有4个核心线程,新任务无限排队(
如何监控和调优?
关键监控API:
executor.getPoolSize(); // 当前线程数
executor.getActiveCount(); // 正在执行任务的线程数
executor.getQueue().size(); // 队列中等待的任务数
executor.getCompletedTaskCount(); // 已完成任务数
调优建议:
-
CPU密集型任务:
- 推荐线程数 = CPU核数 + 1
- 使用有界队列防止资源耗尽
-
IO密集型任务:
- 推荐线程数 = CPU核数 * (1 + 平均等待时间/平均计算时间)
- 示例:8核CPU,任务50%时间在IO → 约8*(1+1)=16个线程
-
超时控制:
// 让核心线程也能超时回收 executor.allowCoreThreadTimeOut(true);