Java 线程池是多线程编程中的核心组件,它通过复用线程来减少线程创建和销毁的开销,提高系统性能和资源利用率。下面从核心概念、工作原理、使用方式和最佳实践几个方面来讲解。
一、为什么需要线程池
不用线程池的问题:
- 每次任务都
new Thread(),创建/销毁开销大 - 线程数量无法控制,高并发时可能耗尽系统资源(内存、CPU)
- 缺乏统一的任务管理和监控手段
线程池的优势:
- 降低资源消耗:复用已创建的线程
- 提高响应速度:任务到达时无需等待线程创建
- 提高可管理性:统一分配、调优和监控
二、核心类:ThreadPoolExecutor
Java 线程池的核心实现是 java.util.concurrent.ThreadPoolExecutor,它的构造方法有 7 个关键参数:
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
参数详解
| 参数 | 说明 |
|---|---|
| corePoolSize | 核心线程数,即使空闲也会保留(除非设置 allowCoreThreadTimeOut) |
| maximumPoolSize | 线程池允许的最大线程数 |
| keepAliveTime | 超过核心线程数的空闲线程存活时间 |
| workQueue | 存放待执行任务的阻塞队列 |
| threadFactory | 创建线程的工厂,可自定义线程名、优先级等 |
| handler | 队列满且线程数达最大时的拒绝策略 |
三、任务提交后的处理流程
这是线程池最核心的机制,理解它非常重要:
提交任务
│
├─ 核心线程未满? ──是──> 创建核心线程执行
│ │否
├─ 队列未满? ────是──> 任务入队等待
│ │否
├─ 线程数 < max? ─是──> 创建非核心线程执行
│ │否
└─> 执行拒绝策略
关键点:只有当队列满了之后,才会创建超出核心线程数的线程。这个顺序容易被误解。
四、四种拒绝策略
当任务无法被接受时(队列满 + 线程数达 max):
- AbortPolicy (默认):抛出
RejectedExecutionException - CallerRunsPolicy:由提交任务的线程自己执行(起降速作用)
- DiscardPolicy:直接丢弃,不抛异常
- DiscardOldestPolicy:丢弃队列中最老的任务,再尝试提交
也可以自定义实现 RejectedExecutionHandler。
五、Executors 提供的快捷工厂(不推荐)
java
Executors.newFixedThreadPool(n); // 固定线程数,队列无界
Executors.newCachedThreadPool(); // 线程数可无限增长
Executors.newSingleThreadExecutor(); // 单线程,队列无界
Executors.newScheduledThreadPool(n); // 支持定时/周期任务
《阿里巴巴 Java 开发手册》明确禁止使用 Executors 创建线程池,原因:
newFixedThreadPool/newSingleThreadExecutor:使用无界队列LinkedBlockingQueue,任务堆积可能导致 OOMnewCachedThreadPool/newScheduledThreadPool:最大线程数为Integer.MAX_VALUE,可能创建海量线程导致 OOM
推荐手动 new ThreadPoolExecutor(...),明确各参数。
六、线程池大小如何设置
没有固定公式,取决于任务类型:
- CPU 密集型:核心数 + 1(减少上下文切换)
- IO 密集型:核心数 × 2,或更多(线程大部分时间在等待 IO)
更精确的估算:
线程数 = 核心数 × 目标CPU利用率 × (1 + 等待时间/计算时间)
实际生产中建议结合压测调整,并配合监控。
七、常用方法
java
executor.execute(runnable); // 提交无返回值任务
Future<T> f = executor.submit(callable); // 提交有返回值任务
executor.shutdown(); // 平滑关闭,不再接受新任务
executor.shutdownNow(); // 立即关闭,尝试中断运行中任务
executor.awaitTermination(...); // 等待终止
executor.getActiveCount(); // 活跃线程数(监控用)
executor.getQueue().size(); // 队列积压数
八、完整示例
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲存活时间
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadFactory() { // 自定义线程工厂
private final AtomicInteger n = new AtomicInteger(1);
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "my-pool-" + n.getAndIncrement());
t.setDaemon(false);
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
for (int i = 0; i < 200; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
});
}
executor.shutdown();
九、注意事项与最佳实践
- 使用有界队列,避免任务无限堆积导致 OOM
- 给线程起有意义的名称,便于排查问题(如 jstack)
- 合理设置拒绝策略 ,核心业务建议
CallerRunsPolicy或自定义 - 关闭线程池 :应用停止时调用
shutdown(),避免资源泄漏 - 监控线程池:活跃线程数、队列大小、完成任务数、拒绝次数
- 避免死锁:父任务等待子任务时,若父子共用同一线程池可能饿死
- 区分业务隔离:不同业务用不同线程池,避免相互影响
十、延伸
- ScheduledThreadPoolExecutor:支持延迟和周期任务
- ForkJoinPool:分治任务,工作窃取算法(Java 8 parallelStream 底层)
- CompletableFuture:Java 8 异步编排,可指定线程池