ThreadPoolExecutor 基础

      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的基础知识,最重要的是结合应用!会使用,会创造才是真正的学会。我们需要根据实际情况制定出最合理的应用方案,如线程池核心参数的制定,队列的选择,拒绝策略的选择,在发生问题时,理解如何分析线程池内部状态,排查并发问题,例如死锁、资源泄露或性能瓶颈,以及平时的监控报警措施等,防患于未然。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小王师傅66

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值