Java线程池调度任务的过程(非常详细)
线程池的任务调度过程会涉及一些重要参数,比如核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、工作等待队列(workQueue)等。
线程池的任务调度过程有以下几个主要步骤:
1) 提交任务:当外部线程将一个任务提交到线程池时,任务通常会被包装成一个实现了Runnable或Callable接口的对象。
2) 核心线程数检查:线程池会检查当前运行的线程数是否小于核心线程数。如果是,即使有空闲线程,线程池也会创建一个新线程来执行提交的任务。
3) 工作等待队列添加:如果当前运行的线程数达到或超过核心线程数,线程池会尝试将提交的任务放入工作等待队列中。工作等待队列是一个阻塞队列,它用于存储等待执行的任务。
4) 最大线程数检查:如果工作等待队列已满,线程池会检查当前运行的线程数是否小于最大线程数。如果小于,线程池会尝试创建一个新线程来执行任务。
5) 任务拒绝:如果当前运行的线程数达到最大线程数,并且工作等待队列已满,此时新提交的任务将无法处理。线程池会通过其拒绝执行处理器(RejectedExecutionHandler)来执行拒绝策略,例如抛出异常、运行任务的 run() 方法、丢弃任务、将任务加入另一个线程池中等。
6) 任务执行:线程从工作等待队列中取出任务后,会调用任务的 run() 方法。核心线程在默认情况下会一直存活,而非核心线程可能会在空闲一定时间(通过 keepAliveTime 参数设定)后被回收。
在了解了线程池任务调度的主要步骤后,我们看一下任务调度的流程,如下图所示:
线程池的任务调度流程是自动化的,从设计上确保了资源的有效利用,减少了线程创建和销毁的耗时,并提供了对并发任务执行的细粒度控制。
注意,在核心线程数已满,但工作等待队列未满时,不会创建线程来执行任务,而会一直等待,所以如果这 3 个参数设置得不合理可能导致工作等待队列一直添加元素至资源耗尽。
以下是一些主要的钩子方法及其作用:
这些钩子方法对于调试、监控以及增强线程池的行为都很有用。它们默认是空操作,如果需要特定的行为,可以通过继承 ThreadPoolExecutor 类并覆盖这些方法来实现。
例如,我们如果想要记录每个任务的执行时间,可以使用如下代码:
在实现这些方法时,需要注意同步和线程安全的问题,避免引入新的并发问题。这些方法都是在执行任务的线程中调用的,因此,任何长时间运行的操作都可能影响线程池中其他任务的执行。
线程池的任务调度过程有以下几个主要步骤:
1) 提交任务:当外部线程将一个任务提交到线程池时,任务通常会被包装成一个实现了Runnable或Callable接口的对象。
2) 核心线程数检查:线程池会检查当前运行的线程数是否小于核心线程数。如果是,即使有空闲线程,线程池也会创建一个新线程来执行提交的任务。
3) 工作等待队列添加:如果当前运行的线程数达到或超过核心线程数,线程池会尝试将提交的任务放入工作等待队列中。工作等待队列是一个阻塞队列,它用于存储等待执行的任务。
4) 最大线程数检查:如果工作等待队列已满,线程池会检查当前运行的线程数是否小于最大线程数。如果小于,线程池会尝试创建一个新线程来执行任务。
5) 任务拒绝:如果当前运行的线程数达到最大线程数,并且工作等待队列已满,此时新提交的任务将无法处理。线程池会通过其拒绝执行处理器(RejectedExecutionHandler)来执行拒绝策略,例如抛出异常、运行任务的 run() 方法、丢弃任务、将任务加入另一个线程池中等。
6) 任务执行:线程从工作等待队列中取出任务后,会调用任务的 run() 方法。核心线程在默认情况下会一直存活,而非核心线程可能会在空闲一定时间(通过 keepAliveTime 参数设定)后被回收。
在了解了线程池任务调度的主要步骤后,我们看一下任务调度的流程,如下图所示:

线程池的任务调度流程是自动化的,从设计上确保了资源的有效利用,减少了线程创建和销毁的耗时,并提供了对并发任务执行的细粒度控制。
注意,在核心线程数已满,但工作等待队列未满时,不会创建线程来执行任务,而会一直等待,所以如果这 3 个参数设置得不合理可能导致工作等待队列一直添加元素至资源耗尽。
Java自定义调度过程
在线程池任务调度过程中,我们如果想插入自定义的逻辑,可以使用调度器的钩子方法。ThreadPoolExecutor 类中提供了几个钩子方法,这些钩子方法会在任务执行的不同阶段被调用,允许开发者在任务执行前后以及线程池变化时插入自定义的逻辑。以下是一些主要的钩子方法及其作用:
1) beforeExecute()方法
protected void beforeExecute(Thread t, Runnable r);beforeExecute() 方法在每个任务执行前被调用,我们可以重写这个方法来执行初始化资源、记录日志信息、统计任务信息等操作。
2) afterExecute()方法
protected void afterExecute(Runnable r, Throwable t);afterExecute() 方法在每个任务执行后被调用,我们可以用它来进行资源清理、任务执行监控、异常处理等。如果任务在执行过程中抛出异常,这个异常将会作为第二个参数传递给 afterExecute() 方法。
3) terminated()方法
protected void terminated();terminated() 方法在线程池完全终止,即线程池的状态转换到 TERMINATED 状态时调用。我们可以用它来释放线程池持有的资源,或者发送通知告知外部监听者线程池已关闭。
这些钩子方法对于调试、监控以及增强线程池的行为都很有用。它们默认是空操作,如果需要特定的行为,可以通过继承 ThreadPoolExecutor 类并覆盖这些方法来实现。
例如,我们如果想要记录每个任务的执行时间,可以使用如下代码:
public class TimingThreadPoolExecutor extends ThreadPoolExecutor { // ... @Override protected void beforeExecute(Thread t, Runnable r) { super.beforeExecute(t, r); // 记录任务开始时间 } @Override protected void afterExecute(Runnable r, Throwable t) { try { // 记录任务结束时间,计算并记录任务执行时间 } finally { super.afterExecute(r, t); } } @Override protected void terminated() { try { // 释放资源或记录线程池关闭的信息 } finally { super.terminated(); } } }需要注意的是,默认情况下,这些方法是没有实现任何具体操作的。在需要时,我们可以在子类中提供具体实现。同时,在重写这些方法时,为了保证 ThreadPoolExecutor 的正确性,应该在方法的开始或结束处调用 super() 方法。
在实现这些方法时,需要注意同步和线程安全的问题,避免引入新的并发问题。这些方法都是在执行任务的线程中调用的,因此,任何长时间运行的操作都可能影响线程池中其他任务的执行。