线程池
概述
线程池(Thread Pool) 是一种复用线程的并发编程方式:预先创建若干线程,由它们反复执行提交的任务(Task),而不是为每个任务都「 new Thread().start()」。使用线程池可以降低线程创建与销毁的开销、控制并发数量、统一管理任务队列与拒绝策略,是生产环境中处理多任务的推荐做法。Java 通过 Executor 框架(java.util.concurrent)提供线程池支持,核心接口是 ExecutorService,常用实现是 ThreadPoolExecutor。学习前建议已掌握 线程创建与生命周期 与 同步与锁。
为什么使用线程池
若每次需要并发执行任务时都 new Thread(task).start(),会带来以下问题:
- 创建与销毁开销大:线程的创建和销毁涉及操作系统资源,频繁创建会浪费 CPU 与内存。
- 线程数量难以控制:高并发时可能创建大量线程,导致上下文切换频繁甚至 OOM。
- 任务与线程耦合:缺少统一的任务队列、取消、超时等能力。
线程池通过固定或可伸缩的一组工作线程 + 任务队列,实现任务与线程解耦、复用线程、限制并发度,从而更稳定、可控。
核心接口与类
| 类型 | 说明 |
|---|---|
| Executor | 顶层接口,仅定义 void execute(Runnable command) |
| ExecutorService | 继承 Executor,增加 submit、shutdown、invokeAll 等生命周期与任务提交方法 |
| ThreadPoolExecutor | 可配置的线程池实现类,通过构造方法指定核心线程数、最大线程数、队列、拒绝策略等 |
| Executors | 工具类,提供 newFixedThreadPool、newCachedThreadPool 等工厂方法(便捷但部分存在隐患) |
日常开发中,通常通过 Executors 快速获得一个 ExecutorService,或直接使用 ThreadPoolExecutor 的构造方法进行精细配置。
基本用法
使用 Executors 创建线程池
Executors 提供几种常用工厂方法,适合快速上手和简单场景。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsDemo {
public static void main(String[] args) {
// 固定大小的线程池:始终维持固定数量的工作线程
ExecutorService pool = Executors.newFixedThreadPool(2);
// 提交任务:execute 无返回值,适合 Runnable
pool.execute(() -> System.out.println("任务1: " + Thread.currentThread().getName()));
pool.execute(() -> System.out.println("任务2: " + Thread.currentThread().getName()));
pool.execute(() -> System.out.println("任务3: " + Thread.currentThread().getName())); // 会排队等待
// 使用完毕后关闭线程池(不再接受新任务,已提交任务会执行完)
pool.shutdown();
}
}提交任务:execute 与 submit
- execute(Runnable):只提交任务,不关心返回值或异常结果;任务抛出未捕获异常可能导致该工作线程退出(取决于默认的未捕获异常处理)。
- submit(Runnable) 或 submit(Callable<T>):返回 Future,可用来获取结果、取消任务或判断是否完成;任务中的异常会被包装在
Future.get()抛出的ExecutionException中。
import java.util.concurrent.*;
public class SubmitDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
// submit(Callable) 可获取返回值
Future<Integer> future = pool.submit(() -> {
Thread.sleep(100);
return 42;
});
System.out.println("计算结果: " + future.get()); // 阻塞直到得到 42
// submit(Runnable) 返回 Future<?>,get() 得到 null
Future<?> f2 = pool.submit(() -> System.out.println("Runnable 任务"));
f2.get(); // 等待执行完成
pool.shutdown();
}
}关闭线程池:shutdown 与 shutdownNow
- shutdown():温和关闭。不再接受新任务,已提交的任务会继续执行完毕;调用后可根据
awaitTermination等待一段时间或直到结束。 - shutdownNow():立即尝试停止。会中断正在执行的任务、返回尚未执行的任务列表;正在执行的任务是否真正中断取决于任务是否响应中断。
提示
生产代码中建议先 shutdown(),再使用 awaitTermination(timeout, unit) 等待一段时间,若仍未结束再视情况调用 shutdownNow(),并妥善处理返回的未执行任务列表。
ExecutorService pool = Executors.newFixedThreadPool(2);
// ... 提交任务 ...
pool.shutdown();
try {
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(2, TimeUnit.SECONDS)) {
System.err.println("线程池未能完全终止");
}
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}线程池核心参数(ThreadPoolExecutor)
要更精细地控制线程池行为,应直接使用 ThreadPoolExecutor 的构造方法。其核心参数包括:
| 参数 | 含义 |
|---|---|
| corePoolSize | 核心线程数:池中常驻的线程数量,即使空闲也可不回收(取决于 allowCoreThreadTimeOut) |
| maximumPoolSize | 最大线程数:池中允许存在的线程数量上限 |
| keepAliveTime | 非核心线程的空闲存活时间,超时后回收 |
| workQueue | 任务队列:核心线程都在忙时,新任务先进入该队列;队列满时才会在不超过 maximumPoolSize 的前提下创建非核心线程 |
| threadFactory | 创建新线程的工厂,可用来命名线程、设为守护线程等,便于排查问题 |
| handler | 拒绝策略:当队列已满且线程数达到 maximumPoolSize 时,新任务如何被拒绝(抛异常、调用者运行、丢弃等) |
任务提交流程简述
- 若当前线程数 < corePoolSize,则创建新的核心线程执行该任务。
- 否则,将任务放入 workQueue。
- 若队列已满且当前线程数 < maximumPoolSize,则创建非核心线程执行该任务。
- 若队列已满且线程数已达 maximumPoolSize,则执行 RejectedExecutionHandler(拒绝策略)。
使用 ThreadPoolExecutor 构造线程池
import java.util.concurrent.*;
public class ThreadPoolExecutorDemo {
public static void main(String[] args) {
int corePoolSize = 2;
int maxPoolSize = 4;
long keepAlive = 60L;
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAlive,
TimeUnit.SECONDS,
queue,
Executors.defaultThreadFactory(),
handler
);
executor.execute(() -> System.out.println("任务执行: " + Thread.currentThread().getName()));
executor.shutdown();
}
}拒绝策略
当线程池「无法再接受新任务」时(队列满且线程数已达上限),会调用 RejectedExecutionHandler。常用策略如下:
| 策略 | 说明 |
|---|---|
| AbortPolicy(默认) | 抛出 RejectedExecutionException |
| CallerRunsPolicy | 由调用 execute/submit 的线程直接执行该任务,相当于限流回退到调用方 |
| DiscardPolicy | 静默丢弃任务 |
| DiscardOldestPolicy | 丢弃队列中最老的一个任务,然后再次尝试提交当前任务 |
可根据业务选择:需要感知「提交失败」时用 AbortPolicy;希望尽量不丢任务、能接受调用方变慢时可用 CallerRunsPolicy。
注意事项
注意
Executors.newFixedThreadPool(int) 和 Executors.newSingleThreadExecutor() 内部使用无界的 LinkedBlockingQueue。若任务提交速度远大于处理速度,队列会无限增长,可能导致 OOM。生产环境更推荐使用 ThreadPoolExecutor 指定有界队列(如 ArrayBlockingQueue)并设置合理的拒绝策略。
注意
Executors.newCachedThreadPool() 的最大线程数为 Integer.MAX_VALUE,且没有任务队列缓冲。高并发下可能创建大量线程,导致上下文切换剧烈甚至 OOM。仅适合大量短生命周期、低并发的任务,使用时要谨慎评估。
提示
为便于排查问题,建议使用自定义 ThreadFactory 为线程命名(例如 pool-1-task-1),或使用 Guava 的 ThreadFactoryBuilder。
相关链接
- 线程创建与生命周期 — 线程与 Runnable 基础
- 同步与锁 — 线程安全与锁
- 并发工具类简介 — JUC 中的 CountDownLatch、CyclicBarrier 等
- Oracle Java 文档 - ExecutorService