ThreadPoolExecutor 类是Java并发包(java.util.concurrent)中用于创建和管理线程池的核心类,在本文中,我们将介绍一些关于ThreadPoolExecutor 基础内容。
一、核心功能包括
1. 任务提交与执行:
将 Runnable 或 Callable 任务提交到线程池,并由线程池中的工作线程执行这些任务。
2. 线程管理和复用:
根据配置参数动态地创建、管理和销毁线程,实现线程的复用,避免频繁创建和销毁线程带来的性能开销。
3. 任务队列管理:
通过内部的任务队列存储等待执行的任务,支持阻塞队列,当线程空闲时从队列中获取任务进行处理。
4. 饱和策略:
在线程池满载或者任务队列已满的情况下,可以通过用户自定义的 RejectedExecutionHandler 实现来决定如何处理新提交的任务。
5.线程生命周期控制:
可以根据核心线程数和最大线程数限制、以及线程空闲超时时间来控制线程池内线程的数量。
二、ThreadPoolExecutor 的核心参数
1. corePoolSize:
线程池的基本大小,即使在没有任务执行时也会保留这么多数量的线程。当有新的任务提交时,如果当前线程数小于 corePoolSize,则会优先创建新的线程来执行任务。
2. maximumPoolSize:
线程池允许的最大线程数,超过这个数量的任务将会被拒绝或放入队列等待。
3. workQueue:
任务队列,通常是一个BlockingQueue实现,用于存储等待执行的任务。
4. keepAliveTime:
非核心线程闲置时,在多长时间后会被终止,只有当线程池数量大于核心线程数时才适用。
5. unit:
与 keepAliveTime 配合使用的单位,如 TimeUnit.SECONDS 或 TimeUnit.MILLISECONDS 等。
6. threadFactory:
用于创建新线程的工厂,可以定制线程的名称、优先级等属性。
7. handler:
RejectedExecutionHandler 接口实现,用来定义当任务无法被接受时(例如线程池关闭或队列已满)的行为策略。
以上参数共同决定了线程池的工作方式,包括线程创建、任务调度、线程回收等方面的行为。
三、队列类型
1. ArrayBlockingQueue:
一个基于数组的有界阻塞队列,具有固定容量,当队列满时会阻止插入操作,当队列为空时会阻止获取操作。
2. LinkedBlockingQueue:
一个基于链表结构的可选有界或无界的阻塞队列。默认情况下,如果不指定容量,则创建的是一个无界队列;如果指定了容量,则创建的是有界队列。它内部采用链表存储元素,并通过两个独立锁分别控制对队列头部和尾部的操作,从而提高了并发性能。
3. SynchronousQueue:
一个特殊的无缓冲队列,它不存储元素,而是直接将生产者提交的任务转发给消费者。每个插入操作必须等待另一个线程的移除操作,反之亦然。因此,此队列通常配合适当的线程池大小来实现工作窃取(Work Stealing)算法,非常适合于Fork/Join框架。
4. PriorityBlockingQueue:
一个支持优先级排序的无界阻塞队列。队列中的元素按照自然顺序或者提供的Comparator进行排序,取出元素时总是获取优先级最高的元素。
这里不得不对比下我常用的ArrayBlockingQueue和LinkedBlockingQueue 作为有界阻塞队列使用时的区别(结合该数据结构分析):
1. 数据结构和内存分配:
ArrayBlockingQueue 基于数组实现,创建时需要指定固定的容量,并且一旦创建,容量不可变。插入或删除元素时不需要额外的内存分配。
LinkedBlockingQueue 基于链表实现,插入和删除操作会涉及节点对象(Node)的创建和销毁,因此在处理大量任务时可能会有更多的内存开销。
2. 并发控制:
ArrayBlockingQueue 使用单一锁来控制对队列的操作,对于生产和消费都是基于同一个锁。
LinkedBlockingQueue 使用两个独立的锁,一个用于生产者(put操作),另一个用于消费者(take操作)。这种分离锁设计可以减少锁竞争,提高并行性能。
3. 扩容行为:
ArrayBlockingQueue 由于其基于固定大小数组,不支持动态扩容,一旦队列满载,再尝试添加任务将会阻塞或者根据拒绝策略处理。
LinkedBlockingQueue 即使定义为有界队列,由于内部实现机制不同,在容量已满时的行为是一致的:新提交的任务会被阻塞等待空间。
4. 性能差异:
ArrayBlockingQueue 因其简单的数组结构,在特定条件下(如连续的入队出队操作,尤其是队列未满或未空的情况下)可能具有更好的性能。
LinkedBlockingQueue 的工作窃取机制可能导致更多的内存分配和释放操作,但得益于分离锁的设计,高并发场景下性能表现可能更优。
四、拒绝策略
当线程池和工作队列都达到其容量限制时,新提交的任务将无法立即执行,此时就需要采用拒绝策略来处理这些超出处理能力的任务。ThreadPoolExecutor 提供了四种内置的拒绝策略:
1. AbortPolicy(默认策略):
当任务被拒绝添加到线程池时,抛出一个 RejectedExecutionException 异常,终止当前任务的执行。
2. CallerRunsPolicy:
当任务被拒绝时,调用者所在的线程会直接执行这个任务,而不是由线程池中的工作线程执行。这样做的结果是,提交任务的线程必须等待自己的任务完成才能继续其他操作,这通常会导致性能下降,但也可能有助于减轻系统的负载压力。
3. DiscardPolicy:
直接丢弃被拒绝的任务,并不抛出异常,也不执行任何其他操作。这种方式可能会丢失数据或任务逻辑未被执行。
4. DiscardOldestPolicy:
尝试丢弃工作队列中最旧的一个未处理的任务(即最早进入队列但还未开始执行的任务),然后重新尝试提交当前被拒绝的任务。如果新的任务再次无法被接受,则重复此过程。
用户还可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略,以满足特定应用场景的需求。例如,可以选择记录日志、发送警告信息或者采取其他更灵活的处理方式。
五、简化的 ThreadPoolExecutor
从任务提交到最终被执行的完整流程概述
1. 任务提交:
当调用 execute(Runnable task) 方法时,会尝试将一个 Runnable 任务提交给线程池。
2. 核心线程创建:
首先检查当前活动线程(包括核心线程和非核心线程)数量是否少于核心线程数(corePoolSize)。如果是,则创建一个新的工作线程来执行新提交的任务。
如果当前线程数等于核心线程数,则进入下一步。
3. 工作队列处理逻辑:
将任务尝试放入工作队列中。如果工作队列未满(例如使用的是无界队列或者有界队列还有剩余空间),则直接将任务添加至队列等待执行。
如果工作队列已满,则继续进行下一步。
4. 非核心线程创建:
检查当前线程总数是否小于最大线程数(maximumPoolSize)。如果当前线程数小于最大线程数,并且能够创建新的非核心线程,则创建一个新线程来执行任务。
如果无法创建新的非核心线程(比如已经达到最大线程数),则触发饱和策略。
5. 饱和策略选择与执行:
见 四、拒绝策略
用户可以通过设置 RejectedExecutionHandler 自定义饱和策略,在达到线程池饱和状态时采取特定的行为。
6. 线程执行任务:
工作线程在空闲时会从工作队列中取出并执行任务。当所有任务都执行完毕并且没有新的任务提交时,非核心线程可能根据 keepAliveTime设置的超时时间被回收,以节省系统资源。
7. 线程关闭与终止:
当调用 shutdown() 或 shutdownNow()方法时,线程池开始拒绝新提交的任务,并试图完成所有已提交但尚未开始执行的任务。
shutdownNow()还会尝试中断正在运行的任务,并返回尚未开始执行的任务列表。
核心线程能否在空闲时也能被回收呢?
在Java的 ThreadPoolExecutor 中,核心线程通常在创建后会一直存活,即使它们处于空闲状态。默认情况下,核心线程不会因为长时间不处理任务而被终止。
但是,通过调用 ThreadPoolExecutor 的 allowCoreThreadTimeOut(boolean value) 方法,并传入 true 参数,可以更改此行为。这样设置之后,核心线程在经历了一定长度的空闲时间(由构造函数中指定的 keepAliveTime 参数决定)后也会被终止,类似于非核心线程的超时回收机制。
六、常见的线程池应用
在Java中,ThreadPoolExecutor 是线程池的核心实现类,虽然它本身并不直接提供几种预定义的线程池类型,但 java.util.concurrent.Executors 工具类提供了几个静态工厂方法来创建不同特性的线程池,这些可以看作是常用的线程池类型:
1. FixedThreadPool(固定大小线程池):
ExecutorService executor = Executors.newFixedThreadPool(nThreads);
这种类型的线程池始终保持指定数量的活动线程,即使某些线程空闲也是如此。当所有线程都处于活动状态时,新提交的任务将等待,直到有线程可用。
2. CachedThreadPool(可缓存线程池):
ExecutorService executor = Executors.newCachedThreadPool();
这个线程池会根据需要创建新的线程,并在长时间空闲后回收它们以减少资源消耗。这意味着如果任务提交的速度超过处理速度,它可以创建任意多的线程来执行任务。
3. SingleThreadExecutor(单线程线程池):
ExecutorService executor = Executors.newSingleThreadExecutor();
这种线程池只有一个工作线程,因此所有任务都将按照它们被提交的顺序进行执行,适合于处理按序执行且不需要并发的任务。
4. ScheduledThreadPool(定长调度线程池):
ScheduledExecutorService executor = Executors.newScheduledThreadPool(corePoolSize);
这个线程池不仅可以执行 Runnable 任务,还可以调度周期性或延迟执行的任务。核心线程数可以根据参数指定,一旦创建,即使没有任务提交,也不会被销毁。
总结:
以上我们整理了一些关于ThreadPoolExecutor的基础知识,最重要的是结合应用!会使用,会创造才是真正的学会。我们需要根据实际情况制定出最合理的应用方案,如线程池核心参数的制定,队列的选择,拒绝策略的选择,在发生问题时,理解如何分析线程池内部状态,排查并发问题,例如死锁、资源泄露或性能瓶颈,以及平时的监控报警措施等,防患于未然。