引言
一、为什么需要线程池?
二、Executor框架总览
三、核心线程池实现
四、ThreadPoolExecutor:灵魂所在
五、Future与异步结果获取
六、总结与最佳实践
互动环节
引言
在Java并发编程的世界里,"为每个任务创建一个新线程"是一种简单却危险的做法。线程的创建和销毁开销巨大,无节制的线程创建会耗尽系统资源,导致应用崩溃。
如何高效地管理线程生命周期,平衡系统资源与任务执行需求?
java.util.concurrent.Executor框架正是JDK给出的完美解决方案。它提供了一种将任务提交与任务执行分离的机制,而线程池是其核心实现。掌握Executor,意味着你真正踏入了高性能Java应用开发的大门。
一、为什么需要线程池?
线程是一种昂贵的资源:
创建开销大:需要调用操作系统内核API,消耗CPU周期。
销毁开销大:同样需要系统调用。
资源消耗:每个线程都需要为其分配栈内存(默认通常为1MB),大量线程会消耗大量内存。
线程池的核心思想:线程复用。预先创建好一定数量的线程放在"池"中,有任务来时,从池中取出一个线程来执行,执行完毕后线程不销毁,而是返回池中等待下一个任务。这极大地减少了创建和销毁线程的开销。
优势:
降低资源消耗:通过复用已创建的线程,减少线程创建和销毁造成的消耗。
提高响应速度:当任务到达时,无需等待线程创建即可立即执行。
提高线程的可管理性:线程是稀缺资源,使用线程池可以进行统一的分配、调优和监控。
二、Executor框架总览
Executor框架是一个用于统一任务执行和调度的接口体系,它解耦了任务提交(Runnable/Callable) 和任务执行(如何运行线程、线程如何调度)。
核心接口关系图:
Executor (基础接口)
|
|-> ExecutorService (核心接口,提供生命周期管理、异步任务提交)
|
|-> AbstractExecutorService (抽象实现类)
|
|-> ThreadPoolExecutor (核心线程池实现)
|
|-> ScheduledExecutorService (定时任务接口)
|
|-> ScheduledThreadPoolExecutor (定时任务线程池实现)
Executor:最基础的接口,只定义了一个方法void execute(Runnable command)。
ExecutorService:继承了Executor,增加了submit(提交任务,可返回Future)、shutdown(优雅关闭)、invokeAll(批量执行任务)等关键方法。
ScheduledExecutorService:继承了ExecutorService,增加了schedule(延迟执行)、scheduleAtFixedRate(固定频率执行)等定时调度方法。
三、核心线程池实现
JDK通过Executors工厂类提供了几种常用的线程池配置方案。
1.newFixedThreadPool- 固定大小线程池
创建一个固定线程数的线程池,任务队列是无界的(LinkedBlockingQueue)。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); // 固定5个线程
for (int i = 0; i < 10; i++) {
final int taskId = i;
fixedThreadPool.execute(() -> {
System.out.println("执行任务 " + taskId + ",线程:" + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 输出:你会发现最多只有5个线程在交替执行10个任务
fixedThreadPool.shutdown(); // 优雅关闭,等待已提交任务执行完毕
适用场景:适用于处理CPU密集型的任务,需要限制当前线程数量,防止资源耗尽。
2.newCachedThreadPool- 可缓存线程池
核心线程数为0,最大线程数为Integer.MAX_VALUE。线程空闲存活时间为60秒。使用SynchronousQueue(不存储元素的阻塞队列)。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int taskId = i;
cachedThreadPool.execute(() -> {
System.out.println("执行任务 " + taskId + ",线程:" + Thread.currentThread().getName());
});
}
// 输出:可能会为每个任务创建一个新线程
cachedThreadPool.shutdown();
适用场景:适用于执行大量短期异步任务的程序,或者负载较轻的服务器。注意:任务提交速度过快时,可能创建大量线程导致OOM。
3.newSingleThreadExecutor- 单线程线程池
只有一个线程的线程池,任务队列无界。保证所有任务按提交顺序串行执行。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
final int taskId = i;
singleThreadExecutor.execute(() -> {
System.out.println("任务 " + taskId + " 正在执行...");
});
}
// 输出:所有任务按顺序依次执行,只有一个工作线程
singleThreadExecutor.shutdown();
适用场景:需要保证任务顺序执行,且任意时间点只有一个任务在执行(例如,日志记录、GUI事件分发)。
4.newScheduledThreadPool- 定时任务线程池
用于执行定时或周期性任务。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
// 1. 延迟执行一次
scheduledThreadPool.schedule(() -> {
System.out.println("延迟5秒后执行!");
}, 5, TimeUnit.SECONDS);
// 2. 固定频率执行(以上一次任务开始时间为起点)
scheduledThreadPool.scheduleAtFixedRate(() -> {
System.out.println("ScheduleAtFixedRate,开始时间间隔2秒");
}, 1, 2, TimeUnit.SECONDS);
// 3. 固定延迟执行(以上一次任务结束时间为起点)
scheduledThreadPool.scheduleWithFixedDelay(() -> {
System.out.println("ScheduleWithFixedDelay,结束时间间隔2秒");
}, 1, 2, TimeUnit.SECONDS);
// 注意:实际应用中需要适当时候调用 shutdown
// scheduledThreadPool.shutdown();
适用场景:需要执行定时任务、轮询任务(如心跳检测、数据同步)。
四、ThreadPoolExecutor:灵魂所在
Executors工厂方法创建的线程池,其底层都是ThreadPoolExecutor实例。理解它的构造参数是灵活使用线程池的关键。
- 核心构造参数(7个)
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数:即使空闲也会保留的线程数(除非allowCoreThreadTimeOut为true)
int maximumPoolSize, // 最大线程数:池中允许存在的最大线程数
long keepAliveTime, // 空闲线程存活时间:非核心线程空闲超过此时间将被终止
TimeUnit unit, // 存活时间单位
BlockingQueue workQueue, // 工作队列:用于保存等待执行的任务的阻塞队列
ThreadFactory threadFactory, // 线程工厂:用于创建新线程
RejectedExecutionHandler handler // 拒绝策略:当线程池和队列都饱和时,如何处理新提交的任务
) - 任务调度流程(重要!)
这是线程池最核心的工作原理,可以概括为以下步骤:
核心线程执行:当提交一个新任务时,如果当前运行的线程数 < corePoolSize,即使有空闲线程,也会创建一个新的核心线程来执行任务。
放入队列:如果运行的线程数 >= corePoolSize,新任务会被放入workQueue等待。
创建非核心线程:如果队列已满,且运行的线程数 < maximumPoolSize,会创建一个新的非核心线程来立即执行这个新任务(而不是排队)。
拒绝任务:如果队列已满,且运行的线程数已达到maximumPoolSize,此时线程池“饱和”,会触发RejectedExecutionHandler来处理这个新任务。
简单比喻:线程池就像一个公司。
corePoolSize:正式员工数量。
workQueue:待处理的任务队列。
maximumPoolSize:公司总人数上限(正式+外包)。
keepAliveTime:外包员工空闲多久后解聘。
RejectedExecutionHandler:任务多到连外包都处理不完时,公司采取的拒绝策略。
- 四种拒绝策略
当线程池和队列都饱和时,会触发拒绝策略。JDK提供了4种内置策略:
AbortPolicy(默认):直接抛出RejectedExecutionException异常。
CallerRunsPolicy:让提交任务的调用者线程自己来执行这个任务。这提供了一个简单的反馈机制,可以减慢新任务提交的速度。
DiscardPolicy:默默丢弃无法处理的任务,不做任何通知。
DiscardOldestPolicy:丢弃队列中最老的一个任务(即下一个将要被执行的任务),然后尝试重新提交当前任务。
自定义线程池示例:
// 创建一个更可控的自定义线程池
ThreadPoolExecutor customExecutor = new ThreadPoolExecutor(
2, // 核心2个线程
5, // 最多5个线程
60L, TimeUnit.SECONDS, // 非核心线程空闲60秒后回收
new ArrayBlockingQueue<>(50), // 使用有界队列,防止队列无限增长
Executors.defaultThreadFactory(), // 使用默认线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 饱和时让调用者线程执行
);
// 使用...
customExecutor.execute(() -> System.out.println("Custom task running!"));
// ... 最后务必关闭
customExecutor.shutdown();
五、Future与异步结果获取
ExecutorService的submit方法可以提交Callable或Runnable任务,并返回一个Future对象,用于获取异步任务的执行结果或状态。
ExecutorService executor = Executors.newFixedThreadPool(2);
// 1. 提交Callable任务,Future获取结果
Future future = executor.submit(() -> {
Thread.sleep(1000); // 模拟耗时计算
return 42; // 返回计算结果
});
// 2. 在主线程中,可以通过Future对象获取结果(会阻塞)
try {
Integer result = future.get(); // 阻塞,直到任务完成并返回结果
System.out.println("计算结果: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 3. 也可以尝试超时获取
try {
Integer result = future.get(500, TimeUnit.MILLISECONDS); // 最多等500ms
} catch (TimeoutException e) {
System.out.println("计算超时,取消任务");
future.cancel(true); // 尝试中断任务执行
}
executor.shutdown();
六、总结与最佳实践
线程池选择:
CPU密集型(计算为主):建议使用FixedThreadPool,核心数设为CPU核数 + 1。
IO密集型(网络、磁盘IO为主):建议使用CachedThreadPool或自定义ThreadPoolExecutor,设置较大的最大线程数(如 CPU核数 * 2 或更大),因为线程大部分时间在阻塞等待。
强烈推荐使用自定义的ThreadPoolExecutor:
Executors工厂方法创建的FixedThreadPool和SingleThreadExecutor使用的任务队列是无界的(LinkedBlockingQueue),可能堆积大量请求,导致OOM。
CachedThreadPool和ScheduledThreadPool允许创建的最大线程数是Integer.MAX_VALUE,可能创建大量线程,导致OOM。
最佳实践:根据业务场景,使用ThreadPoolExecutor构造函数,指定有界队列和明确的拒绝策略。
务必关闭线程池:应用结束时,调用shutdown()或shutdownNow()来关闭线程池,否则JVM可能无法退出。
合理配置参数:没有万能配置,需要根据实际任务特性(CPU/IO密集型)、系统资源进行压测和调优。
Executor框架是Java并发编程的基石之一,它将复杂的线程管理抽象为简单的API,让我们能更专注于业务逻辑。理解其原理,特别是ThreadPoolExecutor的工作机制,是构建高并发、高性能、高稳定性Java应用的必备技能。
互动环节
你在项目中是如何使用线程池的?是直接使用Executors的工厂方法,还是自定义ThreadPoolExecutor?遇到过哪些棘手的线程池问题(比如任务堆积、性能调优)?欢迎在评论区分享你的经验和踩坑故事!