Java线程池的工作流程
第一部分:线程池的核心参数
以 ThreadPoolExecutor 最完整的构造函数为例,它有7个核心参数:
java
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)下面我们逐一解释:
1. corePoolSize - 核心线程数
- 定义:线程池中长期存活的、即使处于空闲状态也不会被销毁的线程数量(除非设置了
allowCoreThreadTimeOut为 true)。 - 作用:它们是线程池的基本“劳动力”。
- 工作流程:当有新任务提交时,线程池会优先创建核心线程来处理任务,即使有其他核心线程空闲也会创建,直到达到
corePoolSize。
2. maximumPoolSize - 最大线程数
- 定义:线程池允许创建的最大线程数量。
- 作用:这是线程池的“劳动力”上限。
- 工作流程:当核心线程都在忙,并且工作队列也已经排满时,线程池才会创建新的临时线程(非核心线程)来处理任务,但总线程数不能超过
maximumPoolSize。
3. keepAliveTime + unit - 线程存活时间
- 定义:当线程数超过
corePoolSize时,那些多余的“临时”线程在空闲状态下等待新任务的最长时间。如果超过这个时间还没有新任务,它们就会被销毁。 - 单位:
unit参数是keepAliveTime的时间单位(如TimeUnit.SECONDS)。 - 作用:为了节省系统资源,避免在低负载时维持过多线程。
4. workQueue - 工作队列
- 定义:一个用于存放待处理任务的阻塞队列(
BlockingQueue)。 - 作用:在任务激增时,起到缓冲和排队的作用。
- 常用队列类型:
LinkedBlockingQueue(无界队列):任务可以无限堆积,会导致maximumPoolSize参数失效,永远不会创建超过corePoolSize的线程。可能引起内存溢出。ArrayBlockingQueue(有界队列):队列大小固定。这是最常用的场景,可以配合maximumPoolSize一起防止资源耗尽。SynchronousQueue(同步移交队列):它不存储元素。每个插入操作必须等待另一个线程的移除操作。因此,如果没有立即可用的线程来执行任务,就会创建新线程(如果没达到最大线程数),否则任务会被拒绝。这要求线程池的maximumPoolSize足够大。
5. threadFactory - 线程工厂
- 定义:用于创建新线程的工厂。
- 作用:可以自定义线程的名称、优先级、是否为守护线程等,方便问题排查和监控。如果不指定,会使用默认的
Executors.defaultThreadFactory()。
6. handler - 拒绝策略
- 定义:当线程池已经关闭,或者线程池和工作队列都已达到饱和状态(即所有线程都在忙且队列已满)时,对新提交的任务采取的处理策略。
- 作用:这是系统的“安全阀”,防止任务无限堆积导致系统崩溃。
第二部分:线程池的工作流程(参数如何协同工作)
理解参数后,我们再通过一个流程图来看它们是如何配合的:
mermaid
flowchart TD
A[提交新任务] --> B{核心线程<br>是否已满?<br>(< corePoolSize)}
B -- 否 --> C[创建新的核心线程执行任务]
B -- 是 --> D{工作队列<br>是否已满?}
D -- 否 --> E[将任务放入工作队列]
D -- 是 --> F{线程总数<br>是否已达上限?<br>(< maximumPoolSize)}
F -- 否 --> G[创建新的临时线程执行任务]
F -- 是 --> H[执行拒绝策略]- 任务提交。
- 如果当前运行的线程数 小于
corePoolSize,则创建新线程(核心线程)来执行任务。 - 如果当前运行的线程数 大于等于
corePoolSize,则尝试将任务放入工作队列 (workQueue)。 - 如果工作队列已满,则尝试创建新的临时线程(非核心线程)来执行任务,直到线程数达到
maximumPoolSize。 - 如果线程数也已达到
maximumPoolSize,并且队列也满了,则触发拒绝策略 (handler)。
第三部分:拒绝策略 (RejectedExecutionHandler)
JDK 内置了四种常见的拒绝策略,它们都实现了 RejectedExecutionHandler 接口。
1. AbortPolicy - 中止策略(默认)
- 行为:直接抛出
RejectedExecutionException异常。 - 优点:明确地通知调用者任务被拒绝了,不会无声无息地丢失任务。
- 缺点:调用者需要捕获并处理异常。
- 适用场景:大多数关键业务场景,需要及时感知到系统过载。
2. CallerRunsPolicy - 调用者运行策略
- 行为:不抛弃任务,也不抛异常,而是将某些被拒绝的任务回退给调用者线程来执行。
- 优点:
- 不会丢失任务。
- 提交任务的线程(调用者)需要自己去执行任务,这会占用调用者的时间,从而减缓了新任务提交的速度,相当于一种负反馈机制,给了线程池一些喘息的时间。
- 适用场景:不允许任务丢失,且能承受任务执行速度变慢的场景。例如,可以在Web服务器中防止队列过载时,将压力回传到数据源(如数据库连接池)。
3. DiscardPolicy - 丢弃策略
- 行为:默默地将无法处理的任务丢弃,不做任何通知。
- 优点:实现简单。
- 缺点:任务无声无息地丢失了,很难发现问题。
- 适用场景:不推荐使用,除非你非常确定可以丢弃某些不重要的任务(如日志记录、心跳检测等)。
4. DiscardOldestPolicy - 丢弃最老策略
- 行为:丢弃工作队列中队首(即下一个将要被执行)的旧任务,然后尝试重新提交当前这个新任务。
- 优点:给了新任务一次机会。
- 缺点:可能会丢弃非常重要的老任务。
- 适用场景:后到来的任务优先级更高的场景。例如,一个实时消息系统,新的消息比旧的消息更有价值。
总结与建议
| 参数 | 说明 | 设置建议 |
|---|---|---|
corePoolSize | 常驻核心线程数 | 根据任务类型(CPU密集型/IO密集型)和机器配置来定。 |
maximumPoolSize | 最大线程数 | 设置一个上限,防止资源耗尽。通常和 corePoolSize 及队列容量一起权衡。 |
workQueue | 任务队列 | 推荐使用有界队列(如 ArrayBlockingQueue),配合拒绝策略,避免资源耗尽。 |
keepAliveTime | 线程空闲时间 | 对于突发流量的系统,可以设置得短一些,以便及时回收资源。 |
handler | 拒绝策略 | 生产环境强烈建议使用 AbortPolicy 或 CallerRunsPolicy,避免使用默认的或丢弃策略,以便及时发现问题。 |
最佳实践:
- 不要使用
Executors的快捷方法(如newFixedThreadPool,newCachedThreadPool),因为它们使用无界队列或无限最大线程数,容易导致OOM。 - 应该根据实际情况,手动创建
ThreadPoolExecutor,并为其指定合适的参数,特别是使用有界队列和明确的拒绝策略。 - 对于需要记录被拒绝任务的场景,可以自定义拒绝策略,在拒绝任务前记录日志。