描述线程池的工作流程及其任务调度策略。
参考回答
线程池的工作流程主要分为以下几个阶段:
- 任务提交:
- 通过
execute()
或submit()
方法提交任务到线程池。
- 通过
- 线程分配与任务队列:
- 如果当前运行的线程数小于核心线程数(
corePoolSize
),创建一个新的线程来执行任务。 - 如果核心线程数已满,则将任务加入队列等待。
- 如果队列也满了,并且线程数小于最大线程数(
maximumPoolSize
),则创建一个非核心线程来执行任务。
- 如果当前运行的线程数小于核心线程数(
- 拒绝任务:
- 当线程数达到最大线程数并且队列也满时,线程池会根据拒绝策略决定如何处理任务,例如丢弃任务、抛出异常或回退到调用线程。
- 任务执行与线程复用:
- 线程从任务队列中获取任务执行,执行完成后不会被销毁,而是继续等待新的任务。
- 线程回收:
- 如果线程数超过核心线程数(非核心线程)并且这些线程在
keepAliveTime
内没有执行任务,会被回收以释放资源。
- 如果线程数超过核心线程数(非核心线程)并且这些线程在
详细讲解与拓展
1. 工作流程图解
线程池的工作流程可以分解为以下步骤:
- 提交任务
- 用户提交任务到线程池(
execute()
或submit()
)。 - 任务可以是实现了
Runnable
或Callable
接口的对象。
- 用户提交任务到线程池(
- 核心线程处理
- 如果当前运行线程数小于核心线程数(
corePoolSize
),线程池直接创建一个新的线程来执行任务。
- 如果当前运行线程数小于核心线程数(
- 任务进入队列
- 当线程数达到核心线程数时,新提交的任务会被加入到任务队列中等待。
- 非核心线程处理
- 如果任务队列满了,并且线程数小于最大线程数(
maximumPoolSize
),线程池会创建新的非核心线程来执行任务。
- 如果任务队列满了,并且线程数小于最大线程数(
- 任务拒绝处理
- 如果线程数达到最大线程数并且任务队列也满,线程池会执行拒绝策略。
- 线程销毁
- 非核心线程如果在
keepAliveTime
时间内没有任务执行,会被销毁以释放资源。
- 非核心线程如果在
2. 任务调度策略
任务的调度策略主要由以下几个组件决定:
- 核心线程数(
corePoolSize
)- 保证线程池中有足够的核心线程来处理任务。
- 即使这些线程处于空闲状态,也不会被销毁。
- 任务队列(
BlockingQueue
)- 用于存放等待执行的任务,常见类型包括:
- 无界队列(
LinkedBlockingQueue
):队列大小不限,适合任务执行速度远快于提交速度的场景。 - 有界队列(
ArrayBlockingQueue
):限制队列大小,适合需要控制任务数量的场景。 - 优先级队列(
PriorityBlockingQueue
):任务按照优先级排序,优先级高的任务优先被执行。
- 无界队列(
- 用于存放等待执行的任务,常见类型包括:
- 最大线程数(
maximumPoolSize
)- 决定线程池中最多能创建的线程数量。
- 适合任务高峰时临时扩展线程池容量。
- 拒绝策略(
RejectedExecutionHandler
)- 当线程池和任务队列都已满时,用来处理无法执行的任务:
AbortPolicy
:抛出异常(默认策略)。CallerRunsPolicy
:由提交任务的线程直接执行任务。DiscardPolicy
:丢弃任务,不抛出异常。DiscardOldestPolicy
:丢弃队列中最老的任务。
- 当线程池和任务队列都已满时,用来处理无法执行的任务:
3. 示例代码
工作流程的实现示例:
import java.util.concurrent.*;
public class ThreadPoolWorkflow {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, TimeUnit.SECONDS, // 非核心线程存活时间
new ArrayBlockingQueue<>(2), // 有界任务队列
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 提交任务
for (int i = 1; i <= 8; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(3000); // 模拟任务执行
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
输出分析:
- 核心线程数为 2,最大线程数为 4,任务队列容量为 2。
- 前 4 个任务由核心线程和任务队列处理。
- 第 5 和第 6 个任务会触发非核心线程创建。
- 第 7 和第 8 个任务因线程池和队列已满而触发拒绝策略。
4. 线程池的调优
- 线程数配置
- CPU 密集型任务:线程数应等于 CPU 核心数,避免过多线程争抢 CPU 资源。
- I/O 密集型任务:线程数应略多于核心数(通常为核心数的 2 倍),以弥补 I/O 等待时间。
- 任务队列配置
- 无界队列:适合任务量不确定,但任务执行时间短的场景。
- 有界队列:适合需要限制任务数量,防止资源耗尽的场景。
- 拒绝策略选择
- 重要任务:使用
CallerRunsPolicy
或自定义策略,确保任务不会丢失。 - 非关键任务:使用
DiscardPolicy
或DiscardOldestPolicy
丢弃任务。
- 重要任务:使用
- 监控和调试
- 使用
getPoolSize()
、getActiveCount()
和getQueue().size()
方法监控线程池状态。 - 定期调整核心线程数和队列大小,优化线程池性能。
- 使用
总结
- 工作流程:
ThreadPoolExecutor
通过核心线程数、任务队列、非核心线程以及拒绝策略来调度任务。 - 任务调度策略:基于线程数、队列类型和拒绝策略决定任务执行的顺序和方式。
- 调优建议:根据任务类型配置合适的线程数和队列大小,并选择合适的拒绝策略,避免资源浪费和任务丢失。