知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!
1. 概述
- 定义:
ThreadPoolExecutor是 Java 并发编程中用于管理线程池的核心类,属于java.util.concurrent包。它通过复用线程、减少线程创建和销毁的开销,提高了多线程任务的执行效率。 - 背景:在高并发场景中,频繁创建和销毁线程会消耗大量系统资源。线程池通过预先创建一定数量的线程并复用它们,显著提升了系统性能和资源利用率。
2. 核心作用
- 降低资源消耗:通过复用已创建的线程,减少线程创建和销毁的开销。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,线程池可以统一管理线程的分配、调优和监控,避免无限制创建线程导致的系统资源耗尽和稳定性问题。
3. 线程池的生命周期
ThreadPoolExecutor 使用一个 AtomicInteger 类型的变量(ctl)来同时表示线程池的状态(runState)和线程池中的工作线程数量(workerCount),底层实现是CAS。ctl 的高 3 位表示线程池状态,低 29 位表示线程数量。这种设计避免了多变量之间的不一致性问题,同时减少了锁的使用,提升了性能。
线程池的 5 种状态
-
RUNNING(运行状态)
- 状态值:
RUNNING = 0 - 描述:线程池处于正常运行状态,可以接收新任务并处理队列中的任务。
- 行为:
- 接受新任务。
- 处理工作队列中的任务。
- 状态值:
-
SHUTDOWN(关闭状态)
- 状态值:
SHUTDOWN = 1 - 描述:线程池不再接受新任务,但会继续处理工作队列中已存在的任务。
- 触发条件:调用
shutdown()方法。 - 行为:
- 拒绝新任务。
- 继续处理工作队列中的任务。
- 中断空闲线程。
- 状态值:
-
STOP(停止状态)
- 状态值:
STOP = 2 - 描述:线程池不再接受新任务,也不会处理工作队列中的任务,并尝试中断所有正在执行的任务。
- 触发条件:调用
shutdownNow()方法。 - 行为:
- 拒绝新任务。
- 清空工作队列。
- 中断所有线程(包括正在执行任务的线程)。
- 状态值:
-
TIDYING(整理状态)
- 状态值:
TIDYING = 3 - 描述:线程池中的任务已全部终止,工作线程数为 0,准备执行终止后的钩子方法。
- 触发条件:
- 当线程池从
SHUTDOWN状态转换而来,且工作队列为空且工作线程数为 0。 - 当线程池从
STOP状态转换而来,且工作线程数为 0。
- 当线程池从
- 行为:
- 调用
terminated()钩子方法。
- 调用
- 状态值:
-
TERMINATED(终止状态)
- 状态值:
TERMINATED = 4 - 描述:线程池完全终止,所有资源已释放。
- 触发条件:
terminated()方法执行完毕。 - 行为:
- 线程池完全停止运行。
- 状态值:
状态转换图
stateDiagram
[*] --> RUNNING
RUNNING --> SHUTDOWN: 调用shutdown()
RUNNING --> STOP: 调用shutdownNow()
SHUTDOWN --> TIDYING: 队列和线程池为空
STOP --> TIDYING: 线程池为空
TIDYING --> TERMINATED: terminated()执行完毕
TERMINATED --> [*]
状态转换的触发方法
shutdown():- 将线程池状态从
RUNNING转换为SHUTDOWN。 - 不再接受新任务,但会继续处理队列中的任务。
- 将线程池状态从
shutdownNow():- 将线程池状态从
RUNNING或SHUTDOWN转换为STOP。 - 不再接受新任务,清空队列,并尝试中断所有线程。
- 将线程池状态从
terminated():- 当线程池状态变为
TIDYING时,会自动调用此方法。 - 开发者可以重写此方法,执行一些自定义的清理操作。
- 当线程池状态变为
awaitTermination(long timeout, TimeUnit unit):- 等待线程池状态变为
TERMINATED,或者超时。
- 等待线程池状态变为
示例代码
import java.util.concurrent.*;
public class ThreadPoolLifecycleExample {
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(10) // 工作队列
{
@Override
protected void terminated() {
System.out.println("线程池已终止!");
}
};
// 提交任务
for (int i = 0; i < 10; i++) {
int taskId = i;
executor.execute(() -> {
System.out.println("任务 " + taskId + " 正在执行,线程: " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务执行
} catch (InterruptedException e) {
System.out.println("任务 " + taskId + " 被中断!");
}
});
}
// 关闭线程池
executor.shutdown(); // 转换为 SHUTDOWN 状态
System.out.println("线程池已调用 shutdown()");
// 等待线程池终止
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("线程池未完全终止,调用 shutdownNow()");
executor.shutdownNow(); // 转换为 STOP 状态
}
System.out.println("主线程结束");
}
}
4. 参数详解
核心线程数(corePoolSize)
- 作用:线程池中保持存活的最小线程数,即使这些线程处于空闲状态。
- 默认行为:
- 线程池初始化时不会立即创建核心线程,而是在有任务提交时逐步创建(懒加载思想,不过也可以调用prestartAllCoreThreads()方法启动所有核心线程,这个避免了首次任务的等待时间:消除冷启动延迟,视业务情况决定是否调用,如果设置了
allowCoreThreadTimeOut=true,预启动的线程可能在空闲超时后被销毁,这个要注意下)。 - 如果设置了
allowCoreThreadTimeOut(true),默认是false,核心线程在空闲超时后也会被回收,当是false的时候,核心线程永不销毁,除非线程池被关闭(shutdown()/shutdownNow())
- 线程池初始化时不会立即创建核心线程,而是在有任务提交时逐步创建(懒加载思想,不过也可以调用prestartAllCoreThreads()方法启动所有核心线程,这个避免了首次任务的等待时间:消除冷启动延迟,视业务情况决定是否调用,如果设置了
- 适用场景:
- 适合长期保持一定数量线程的场景。
- 核心线程为什么长期存活?
- 避免冷启动延迟:线程创建需要系统资源,频繁创建/销毁会导致性能抖动。
- 应对突发流量:保留核心线程可快速处理新任务,无需等待线程创建。
- 资源与响应平衡:牺牲少量常驻内存,换取稳定的吞吐量
最大线程数(maximumPoolSize)
- 作用:线程池中允许存在的最大线程数量。
- 默认行为:
- 当任务数量超过核心线程数且工作队列已满时,线程池会创建新线程,直到达到最大线程数,如果线程等待任务的时间超过了keepAliveTime,就会被回收,系统是通过addWorker 方法的 core 参数来区分核心线程和非核心线程的
- 适用场景:
- 适合任务量波动较大的场景。
工作队列(workQueue)
- 作用:用于存放等待执行的任务的队列。
- 常见队列类型:
LinkedBlockingQueue:无界队列,适合任务量较大的场景。ArrayBlockingQueue:有界队列,适合需要控制队列大小的场景。SynchronousQueue:不存储任务的队列,适合任务量较小且需要快速响应的场景。PriorityBlockingQueue:支持优先级排序的无界队列。
线程工厂(threadFactory)
- 作用:用于创建新线程的工厂。
- 默认行为:
- 使用
Executors.defaultThreadFactory(),创建的线程具有相同的优先级和非守护线程状态。
- 使用
- 自定义用途:
- 可以自定义线程的名称、优先级、是否为守护线程等。
拒绝策略(RejectedExecutionHandler)
- 作用:当线程池无法接受新任务时,用于处理被拒绝任务的策略。
- 常见策略:
AbortPolicy:直接抛出RejectedExecutionException异常。CallerRunsPolicy:由提交任务的线程直接执行该任务。DiscardPolicy:直接丢弃被拒绝的任务。DiscardOldestPolicy:丢弃队列中最旧的任务,然后重新尝试提交当前任务。- 也可自定义拒绝策略,可以接入告警机制,例如钉钉、企微告警等
5. 工作原理
任务执行流程
- 任务提交:
- 当有新任务提交时,线程池会优先使用核心线程执行任务。
- 任务调度:
- 如果核心线程都在忙碌,任务会被放入工作队列中等待执行。
- 如果工作队列已满且线程数未达到最大线程数,线程池会创建新线程来执行任务。
- 如果线程数已达到最大线程数且队列已满,线程池会触发拒绝策略处理新任务。
- 线程复用:
- 线程池中的线程在执行完任务后不会被销毁,而是保持存活状态,等待下一个任务。
- 线程回收:
- 当线程池中的线程数超过核心线程数时,空闲线程在
keepAliveTime超时后会被回收。
- 当线程池中的线程数超过核心线程数时,空闲线程在
流程图
sequenceDiagram
participant Client as 客户端
participant ThreadPool as 线程池
participant CoreThread as 核心线程
participant Queue as 工作队列
participant NewThread as 新线程
participant RejectPolicy as 拒绝策略
Client->>ThreadPool: 提交任务
alt 是否有空闲核心线程?
ThreadPool->>CoreThread: 使用核心线程执行任务
CoreThread-->>ThreadPool: 任务完成
else 工作队列是否已满?
ThreadPool->>Queue: 将任务放入工作队列
Queue-->>ThreadPool: 任务排队
else 线程数是否达到最大线程数?
ThreadPool->>NewThread: 创建新线程执行任务
NewThread-->>ThreadPool: 任务完成
else
ThreadPool->>RejectPolicy: 触发拒绝策略
RejectPolicy-->>Client: 处理被拒绝的任务
end
6. 线程池的创建方式
1. 固定大小的线程池(FixedThreadPool)
- 特点:线程数量固定,任务队列无界。
- 适用场景:适合任务量稳定且需要限制线程数量的场景。
- 创建方法:
ExecutorService executor = Executors.newFixedThreadPool(int nThreads);
2. 单线程的线程池(SingleThreadExecutor)
- 特点:只有一个线程,任务队列无界。
- 适用场景:适合需要保证任务顺序执行的场景。
- 创建方法:
ExecutorService executor = Executors.newSingleThreadExecutor();
3. 可缓存的线程池(CachedThreadPool)
- 特点:线程数量不固定,空闲线程在 60 秒后被回收。
- 适用场景:适合任务量波动较大的场景。
- 创建方法:
ExecutorService executor = Executors.newCachedThreadPool();
4. 定时任务的线程池(ScheduledThreadPool)
- 特点:支持定时任务和周期性任务。
- 适用场景:适合需要定时执行或周期性执行任务的场景。
- 创建方法:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(int corePoolSize);
5. 自定义线程池
- 特点:通过
ThreadPoolExecutor构造函数显式设置线程池参数。 - 适用场景:适合需要灵活配置线程池参数的场景。
- 创建方法:
ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler );
7. Work
- work是什么?
- Worker是线程池中封装工作线程的类,负责执行任务,并管理线程的生命周期。每个Worker对应一个线程,处理队列中的任务
- work的核心职责
- 线程和任务绑定:
- 每个 Worker 持有一个由线程工厂(ThreadFactory)创建的线程,该线程实际执行的是 Worker 自身的 run() 方法。
- 当通过 addWorker(firstTask, core) 创建 Worker 时,若 firstTask 非 null,线程会优先执行此任务,之后再从队列中获取任务。
- 循环执行任务:
在
runWorker(Worker w)方法中,Worker通过以下流程执行任务: 1、优先执行 firstTask(如果存在) 2、调用 getTask() 【这个方法是有超时时间的】从队列中获取新任务(可能阻塞或超时)。 3、若 getTask() 返回 null,则退出循环,触发线程回收。 - 锁机制(AQS): 通过继承 AbstractQueuedSynchronizer(AQS),Worker 实现了一个不可重入的独占锁,用于防止任务执行期间被中断(当线程正在执行任务时(调用 task.run()),锁被持有,此时中断请求会被暂存,直到任务完成)、线程安全的状态管理(确保 Worker 的中断和状态变更操作是线程安全的)
- 线程和任务绑定:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // 允许中断
boolean completedAbruptly = true;
try {
// 循环获取任务并执行
while (task != null || (task = getTask()) != null) {
w.lock(); // 加锁表示任务正在执行
try {
beforeExecute(wt, task); // 钩子方法(可重写)
task.run(); // 执行任务
afterExecute(task, null); // 钩子方法(可重写)
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly); // 处理线程退出
}
}
8. 思考
- ThreadPoolExecutor 如何维护自身状态?
- 通过
ctl变量维护线程池的状态和工作线程数量,使用位运算高效管理状态转换。
- 通过
- ThreadPoolExecutor 如何维护内部线程?
- 通过
Worker类封装线程和任务,使用HashSet<Worker>维护工作线程集合。
- 通过
- ThreadPoolExecutor 如何提交任务?
- 通过
execute(Runnable command)方法提交任务,根据线程池状态和参数决定任务的执行方式。
- 通过