ThreadPoolExecutor 是 Java 标准库中用于创建和管理线程池的核心类之一。它实现了 ExecutorService 接口,提供了丰富的线程池管理功能。下面将通过源码解析来深入了解 ThreadPoolExecutor 类的工作原理和各个重要部分。
可以在 Java 源代码中找到 ThreadPoolExecutor 类的实现,位于 java.util.concurrent 包中。
以下是 ThreadPoolExecutor 类的一些关键概念和部分:
1、构造函数:
ThreadPoolExecutor 类提供了几个不同的构造函数,允许传递核心线程数、最大线程数、空闲线程存活时间、任务队列等参数。这些参数决定了线程池的基本行为。
下面是几个常用的构造函数及其参数的解释:
核心线程数 (corePoolSize):
核心线程数表示线程池中始终保持存活的线程数量。这些线程会一直存在,即使它们处于空闲状态。线程池会根据任务数量自动创建新线程,直到核心线程数达到上限。最大线程数 (maximumPoolSize):
最大线程数表示线程池中最多可以同时存在的线程数量。当核心线程数已满,且任务队列也已满时,线程池会创建新线程,直到最大线程数达到上限。如果达到最大线程数后仍有更多任务到达,根据饱和策略进行处理。空闲线程存活时间 (keepAliveTime):
空闲线程存活时间表示当线程池中的线程数超过核心线程数,并且这些线程处于空闲状态时,它们会被保留的时间。超过此时间后,多余的空闲线程将被终止,从而节省系统资源。时间单位 (unit):
时间单位用于指定空闲线程存活时间的单位,可以是秒、毫秒等。任务队列 (workQueue):
任务队列用于存储等待执行的任务。线程池可以使用不同类型的队列,如 BlockingQueue 的各种实现。任务队列决定了等待执行的任务数量。线程工厂 (threadFactory):
线程工厂用于创建新线程,默认使用 Executors.defaultThreadFactory()。可以自定义线程工厂来创建线程,从而指定线程的名称、优先级等属性。饱和策略 (handler):
当线程池和任务队列都已满,新的任务到达时,饱和策略定义了如何处理这些任务。线程池提供了几种内置的饱和策略,如 AbortPolicy、CallerRunsPolicy、DiscardPolicy 以及 DiscardOldestPolicy。
2、线程池状态:
ThreadPoolExecutor 的内部维护了几种状态,包括 RUNNING、SHUTDOWN、STOP、TIDYING 和 TERMINATED。线程池在不同的状态下会有不同的行为,例如当调用 shutdown 方法时,线程池会从 RUNNING 状态转变为 SHUTDOWN 状态,不再接受新的任务。
RUNNING(运行中):
在 RUNNING 状态下,线程池处于正常运行状态,可以接受新任务并执行已提交的任务。在这个状态下,核心线程数和非核心线程数都可以创建和执行任务。SHUTDOWN(关闭中):
当调用线程池的 shutdown 方法时,线程池会进入 SHUTDOWN 状态。在这个状态下,线程池不再接受新任务,但会继续执行已提交的任务,包括等待队列中的任务。STOP(立即停止):
当调用线程池的 shutdownNow 方法时,线程池会进入 STOP 状态。在这个状态下,线程池会尝试中断所有正在执行的线程,并清空任务队列。TIDYING(整理中):
当线程池状态从 SHUTDOWN 转变为 TIDYING,表示线程池已经停止接受新任务,正在执行中的任务也已经完成,处于整理和清理状态。在这个状态下,线程池会执行一些清理操作,例如中断空闲线程。TERMINATED(已终止):
当线程池状态从 TIDYING 转变为 TERMINATED,表示线程池已经彻底终止,所有任务都已执行完毕,并且线程池中的所有线程都已销毁。在这个状态下,线程池不再执行任何操作。
3、任务队列:
任务队列用于存储等待执行的任务。ThreadPoolExecutor 允许使用不同类型的队列,如 BlockingQueue 的各种实现,包括 LinkedBlockingQueue、ArrayBlockingQueue 等。这些队列控制了等待执行的任务数量。
以下是几种常见的任务队列类型及其特点:
LinkedBlockingQueue(链式阻塞队列):
LinkedBlockingQueue 是一个基于链表的无界阻塞队列,它可以存储无限数量的任务。在核心线程数未满的情况下,新的任务会直接创建新线程来执行。当核心线程数已满时,任务会被放入队列中等待执行。ArrayBlockingQueue(数组阻塞队列):
ArrayBlockingQueue 是一个基于数组的有界阻塞队列,需要指定队列的容量。在核心线程数未满的情况下,新的任务会直接创建新线程来执行。当核心线程数已满时,任务会被放入队列中等待执行。PriorityBlockingQueue(优先级阻塞队列):
PriorityBlockingQueue 是一个无界阻塞队列,它会根据任务的优先级来进行调度。具有较高优先级的任务会被优先执行。DelayedWorkQueue(延迟工作队列):
DelayedWorkQueue 是一个用于调度延迟任务的队列,其中的任务可以设置延迟执行时间。适用于需要按照一定延迟执行任务的场景。SynchronousQueue(同步队列):
SynchronousQueue 是一个没有实际存储能力的队列,每个插入操作必须等待一个相应的删除操作,反之亦然。适用于需要实现一对一的任务交付机制。
用了无界队列那非核心线程就不会创建了?
是的,当线程池使用无界队列(如 LinkedBlockingQueue 或 PriorityBlockingQueue)时,非核心线程的创建将受到影响。在无界队列中,任务可以无限制地排队等待执行,因此不会触发创建额外的非核心线程。
对于无界队列,以下是一些关键点需要注意:
核心线程数 (corePoolSize): 核心线程数仍然会影响线程池的初始线程创建,当任务提交到线程池时,如果当前活动线程数小于核心线程数,新任务会创建一个核心线程来执行。这适用于线程池的初始阶段或在任务数量较少的情况下。最大线程数 (maximumPoolSize): 如果任务的到达速率超过了核心线程的执行速度,并且任务队列已满,非核心线程将不会创建,因为无界队列可以容纳所有任务。任务队列: 无界队列会持续地存储等待执行的任务,直到系统资源用尽。这可能导致内存占用逐渐增加,因此在选择无界队列时需要注意系统资源的管理。
无界队列适用于一些特定场景,如任务量波动大,任务执行时间差异较大,或者希望尽量保留任务而不丢失的情况。然而,需要注意的是,如果任务数量持续增加,无界队列可能会导致内存消耗过大,因此在选择队列类型时需要综合考虑线程池的整体性能和资源利用。
阻塞队列?
前面提到的 LinkedBlockingQueue、PriorityBlockingQueue 和 ArrayBlockingQueue 都是阻塞队列,这里我将更详细地解释一下阻塞队列的概念以及它们的作用。
阻塞队列是一种特殊类型的队列,具有以下特点:
阻塞特性: 当向队列添加元素或从队列中取出元素时,如果队列已满或为空,阻塞队列会自动阻塞线程,直到队列变为非满或非空为止。线程安全: 阻塞队列是线程安全的,多个线程可以并发地进行入队和出队操作,而不需要额外的同步措施。
在 ThreadPoolExecutor 中,任务队列是一个关键组件,它决定了线程池中等待执行的任务数量、调度策略以及如何处理任务。不同类型的阻塞队列在不同的情况下有不同的用途和特点,可以根据实际需求进行选择。
具体来说:
LinkedBlockingQueue 是一个基于链表的无界阻塞队列。当任务数量超过核心线程数时,新的任务会被放入队列中等待执行。如果队列已满,新任务会阻塞等待直到有空间。
ArrayBlockingQueue 是一个基于数组的有界阻塞队列。它需要指定队列的容量。当队列已满时,新的任务会阻塞等待直到有空间。
PriorityBlockingQueue 是一个无界阻塞队列,根据元素的优先级来进行调度。优先级高的元素会被先出队执行。
请注意,阻塞队列适用于不同的场景和需求。根据应用特性和性能要求,选择适合的阻塞队列类型是很重要的。
LinkedBlockingQueue无界队列?
LinkedBlockingQueue 是一个基于链表的可选界限阻塞队列,而不是无界队列。这意味着它可以选择性地指定队列的容量,当容量未指定时,队列会默认为无界。
所以,当使用 LinkedBlockingQueue 作为任务队列时,如果没有指定容量(或者容量为 Integer.MAX_VALUE),队列会被认为是无界的,新任务总是可以放入队列中,而不会因为队列已满而阻塞。
如果指定了容量,当任务数量超过容量时,新的任务会被放入队列中等待执行。当队列已满时,新任务会阻塞等待直到有空间,这是典型的阻塞队列行为。
4、线程工厂:
ThreadPoolExecutor 允许通过提供线程工厂来自定义线程的创建过程,包括线程的名称、优先级、是否守护线程等属性。线程工厂负责创建新的线程实例,然后线程池会使用这些线程来执行任务。
ThreadPoolExecutor 的构造函数中有一个参数 threadFactory,可以传递一个实现了 ThreadFactory 接口的对象来指定线程工厂。ThreadFactory 接口只有一个方法 newThread,用于创建新的线程。以下是一个简单的示例:
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;public class CustomThreadFactoryExample {public static void main(String[] args) {ThreadFactory threadFactory = new CustomThreadFactory("MyThreadGroup");ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS,new LinkedBlockingQueue<>(), threadFactory);// ... 添加任务到线程池并执行 ...executor.shutdown();}
}class CustomThreadFactory implements ThreadFactory {private final ThreadGroup threadGroup;private final AtomicInteger threadNumber = new AtomicInteger(1);public CustomThreadFactory(String threadGroupName) {SecurityManager s = System.getSecurityManager();threadGroup = (s != null) ? s.getThreadGroup() :Thread.currentThread().getThreadGroup();}public Thread newThread(Runnable r) {Thread thread = new Thread(threadGroup, r,"MyThread-" + threadNumber.getAndIncrement(),0);if (thread.isDaemon())thread.setDaemon(false);if (thread.getPriority() != Thread.NORM_PRIORITY)thread.setPriority(Thread.NORM_PRIORITY);return thread;}
}
在上面的示例中,创建了一个自定义的线程工厂 CustomThreadFactory 实现了 ThreadFactory 接口,用于创建具有自定义属性的线程。在这个例子中,指定了线程组、线程名称前缀等属性。
通过使用线程工厂,可以为线程池中的每个线程设置特定的属性,从而更好地管理和控制线程的行为。这对于在多线程应用程序中调试和监视线程非常有帮助。
5、饱和策略:
当线程池和任务队列都已满,新的任务到达时,饱和策略定义了如何处理这些任务。ThreadPoolExecutor 提供了几种内置的饱和策略,如 AbortPolicy(抛出异常)、CallerRunsPolicy(由调用者线程执行任务)、DiscardPolicy(丢弃任务)以及 DiscardOldestPolicy(丢弃最旧的任务)。
下面对每种策略进行详细解释:
AbortPolicy(抛出异常):
这是默认的饱和策略。当线程池和队列都已满时,新任务会导致 RejectedExecutionException 异常被抛出,提示线程池已经饱和。这是一种保守的策略,防止任务丢失。CallerRunsPolicy(由调用者线程执行任务):
当线程池和队列都已满时,新任务会被调用者线程(提交任务的线程)直接执行,而不会交给线程池中的线程来执行。这种策略可能会导致调用者线程阻塞,因为它们会等待任务执行完毕。DiscardPolicy(丢弃任务):
当线程池和队列都已满时,新任务会被直接丢弃,不会进行任何处理。这可能会导致任务丢失,慎用此策略。DiscardOldestPolicy(丢弃最旧的任务):
当线程池和队列都已满时,会尝试将最早的任务从队列中移除,然后添加新任务。这可能会导致一些旧任务被丢弃,以便为新任务腾出空间。
这些饱和策略提供了不同的处理方式,可以根据应用需求来选择合适的策略。通常情况下,AbortPolicy 是默认的且安全的选择,因为它会在资源不足时抛出异常,提示应该考虑调整线程池大小或处理任务队列。其他策略可能会导致任务丢失或阻塞,需要根据具体情况谨慎选择。
6、线程池的执行过程:
当任务提交给 ThreadPoolExecutor 后,线程池会根据当前状态、核心线程数、任务队列状态等决定任务的处理方式。如果核心线程数未满,会创建新线程来执行任务;如果核心线程数已满,会尝试将任务放入队列,如果队列也已满,会根据饱和策略来处理任务。
描述的流程如下:
核心线程数未满:
如果线程池的当前活动线程数小于核心线程数,线程池会创建一个新的核心线程来立即执行提交的任务。核心线程数已满:
如果线程池的当前活动线程数达到核心线程数,新任务会被放入任务队列等待执行。队列未满:
如果任务队列未满,新任务会被放入队列中等待执行。队列已满:
如果任务队列已满,根据选择的饱和策略来处理任务。可能的策略包括:抛出异常(AbortPolicy):如果饱和策略为 AbortPolicy,则新任务会被拒绝,并抛出 RejectedExecutionException 异常。由调用者线程执行(CallerRunsPolicy):如果饱和策略为 CallerRunsPolicy,则提交任务的线程(调用者线程)会执行该任务,而不会交给线程池中的线程执行。丢弃任务(DiscardPolicy):如果饱和策略为 DiscardPolicy,则新任务会被直接丢弃,不会进行任何处理。丢弃最旧的任务(DiscardOldestPolicy):如果饱和策略为 DiscardOldestPolicy,则尝试从队列中移除最旧的任务,以便为新任务腾出空间。
线程池根据这些步骤来动态调整线程的创建和任务的处理,以适应不同的并发情况和资源限制。这种灵活的处理方式使得线程池能够在不同的负载下保持高效的任务处理能力。
7、线程池的终止:
调用 shutdown 方法会触发线程池的终止过程。线程池会拒绝新的任务,等待已提交但未执行的任务完成,然后关闭线程池中的线程。
调用 shutdown 方法是线程池的一种优雅关闭方式。下面将更详细地解释 shutdown 方法的作用和线程池的终止过程:
调用 shutdown 方法:
当调用线程池的 shutdown 方法时,线程池会开始终止的过程。在此过程中,线程池将不再接受新的任务,但会继续执行已提交但尚未执行的任务,同时等待队列中的任务也会被继续执行。任务执行和队列处理:
在终止过程中,线程池会让已经创建的核心线程和非核心线程继续处理已提交的任务。同时,线程池也会尝试从任务队列中获取任务来执行。如果队列中还有等待执行的任务,线程池会继续分配线程来执行这些任务。拒绝新任务:
在调用 shutdown 方法后,线程池会拒绝接受新的任务。任何尝试提交新任务的操作都会被拒绝,并且会抛出 RejectedExecutionException 异常。等待任务完成:
在终止过程中,线程池会等待队列中的任务和正在执行的任务都完成。这意味着线程池不会立即关闭,而是会等待任务全部执行完毕。关闭线程池中的线程:
一旦所有任务都执行完毕,线程池会关闭其中的线程。如果线程池中存在非核心线程,它们在任务执行完毕后会根据 keepAliveTime 和空闲时间来判断是否终止。终止状态:
当线程池中的所有线程都已关闭时,线程池会达到 TERMINATED 状态,表示线程池已经完全终止。
总之,调用 shutdown 方法后,线程池会等待已提交但未执行的任务完成,并且关闭线程池中的线程,最终达到终止状态。这种方式可以避免任务丢失,并且允许线程池逐步优雅地停止,释放资源。