全面了解ThreadPoolExecutor线程池各项参数

阿里巴巴Java开发者手册中有下面这样一条规定:

如果我们要手动创建一个线程池,必须要了解这个线程池都有哪些参数?这些参数都有什么作用。这样才能更好的创建一个线程池

参数

ThreadPoolExecutor 有如下4个构造方法:

ThreadPoolExecutor 构造方法

这里只看参数最多的那个构造函数如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

ThreadPoolExecutor的创建主要参数有7个,接下来将进行一一介绍。

最大线程数(MaximumPoolSize)和核心线程数(CorePoolSize)

最大线程数和核心线程数是线程池中非常重要的两个参数:

  • 最大线程数(MaximumPoolSize):线程池运行的最大线程数量,受属性CAPACITY的限制,最大为(2^29)-1(约5亿)。当线程池中的线程数目已经达到核心线程数,并且任务队列也已经满了,此时线程池会创建新的线程来执行任务,直到线程数目达到最大线程数
  • 核心线程数(CorePoolSize):线程池中保持最小活动数的线程数量,并且不允许超时,除非调用allowCoreThreadTimeOut方法,这个时候最小值是0。当线程池中的线程数目达到核心线程数时,新任务将会被加入到任务队列中。

默认情况下,当一个任务请求时,核心线程数才会被创建和启动,但是也可以通过prestartCoreThread()方法启动一个核心线程或者prestartAllCoreThreads()启动所有核心线程。

这两个参数对于线程池的性能和响应能力都有很大的影响,一般情况下,应该根据具体的应用场景来进行调整。

如果将核心线程数设置得太小,会导致任务无法及时得到执行,从而降低系统的响应能力;如果将最大线程数设置得太大,会导致系统资源的浪费,并且可能会导致系统崩溃。因此,在设置这两个参数时,需要根据实际情况进行合理的配置。

一般情况下,可以将核心线程数设置为CPU核心数的1-2倍,最大线程数设置为核心线程数的2-4倍,具体的设置需要根据系统负载、任务类型、线程池的容量等多种因素来综合考虑。

新建线程(ThreadFactory)

Java线程池中的ThreadFactory是用来创建线程的工厂接口,它主要有两个作用:

  1. 定义线程的属性:ThreadFactory可以自定义线程的一些属性,如线程名、优先级、是否为守护线程等,从而可以更加灵活地管理线程。
  2. 创建线程实例:ThreadFactory负责创建线程实例,这些线程实例将被添加到线程池中,并被用来执行任务。

Java线程池提供了一个默认的ThreadFactory实现类——DefaultThreadFactory,它会创建普通的、非守护线程,并使用简单的编号来命名线程。如果需要自定义线程的属性,可以实现自己的ThreadFactory类并传递给线程池进行使用。ThreadFactory如果调用newThread(Runnable r)方法返回null则表示创建线程失败,但线程池会继续运行但可能不会执行任何任务。

ThreadFactory的作用并不仅限于创建线程实例,它还可以用来记录日志、监控线程状态等,因此在实际开发中,可以根据具体的需求自定义ThreadFactory实现类来满足不同的需求。

存活时间(Keep-alive times)

存活时间(Keep-alive times):空闲线程等待工作的超时时间(以纳秒为单位)。 当空闲线程闲置时间超过设置的存活时间时,那么他们将会被终止运行,资源将被回收。当线程池不被频繁使用的时候,这提供了一种减少资源消耗的方法。存活时间可以通过setKeepAliveTime(long, TimeUnit)方法进行修改,使用 setKeepAliveTime(Long.MAX_VALUE, NANOSECONDS)有效地禁止空闲线程在关闭之前终止。

需要注意的是,默认情况下,线程池中的核心线程不会受到存活时间的影响,只有非核心线程(即在核心线程已经满员的情况下创建的线程)才会受到存活时间的影响。当一个线程被回收之后,如果后续有新的任务需要执行,线程池会重新创建一个新的线程来执行该任务。如果线程池中的线程数量超过了最大线程数,新的任务将会被放到等待队列中,等待线程池中的线程完成任务之后再执行。

属性 keepAliveTime值不为零的情况下,也可以使用方法allowCoreThreadTimeOut(true)将这个超时策略应用到核心线程。

时间单位(TimeUnit)

TimeUnit 是存活时间的单位。可选值如下:

TimeUnit.NANOSECONDS; //纳秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.MILLISECONDS; // 毫秒
TimeUnit.SECONDS; //秒
TimeUnit.MINUTES; //分钟
TimeUnit.HOURS;  //小时
TimeUnit.DAYS;  //天

阻塞队列(BlockingQueue)

Java线程池中的阻塞队列(Blocking Queue)用于存放等待执行的任务,线程池中的工作线程从队列中取出任务并执行。Java提供了多种类型的阻塞队列,常用的有以下几种:

  1. ArrayBlockingQueue:一个有界的阻塞队列,它的内部实现是一个定长数组。当队列已满时,新的任务将无法加入队列中,直到有工作线程从队列中取出任务。
  2. LinkedBlockingQueue:一个可选有界阻塞队列,在构造函数中可以指定队列的大小。当队列已满时,新的任务将会被阻塞,直到有工作线程从队列中取出任务。
  3. SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的删除操作,否则插入操作将一直阻塞。这种队列通常用于传递数据的场景,例如在线程池中执行RPC调用。
  4. PriorityBlockingQueue:一个支持优先级的无界阻塞队列,元素会按照优先级顺序被取出。如果没有指定优先级,将按照自然排序的顺序进行排序。

阻塞队列的选择取决于应用程序的需求和特性。如果需要控制队列的容量并防止内存泄漏,可以使用ArrayBlockingQueueLinkedBlockingQueue。如果需要实现多个线程之间的交互,可以使用SynchronousQueue。如果需要按照优先级顺序处理任务,可以使用PriorityBlockingQueue

三种排队策略

Java线程池中的任务排队策略(Queueing strategy)决定了当线程池中的工作线程已经全部占用时,后续提交的任务该如何处理。Java线程池中常用的三种任务排队策略如下:

  1. 直接提交策略(Direct handoff)
    直接提交策略又称为Synchronous Queue策略,当线程池中的工作线程已经全部占用时,新任务将会直接交由工作线程执行,而不会被放入队列中等待。这种策略通常用于需要立即处理的任务,但是由于没有队列的缓冲功能,如果线程池的工作线程处理速度较慢,新任务会被阻塞,可能会导致性能下降。

  2. 无界队列策略(Unbounded Queue)
    无界队列策略即LinkedBlockingQueue,这种策略使用一个无界的队列来缓存待执行的任务。当线程池中的工作线程已经全部占用时,新任务将会被放入队列末尾等待执行。由于队列的长度是无限的,所以理论上无界队列策略可以保证所有任务都会被执行,但是当任务的提交速度高于任务执行速度时,队列可能会无限增长,消耗大量内存。

  3. 有界队列策略(Bounded Queue)
    有界队列策略即ArrayBlockingQueue,这种策略使用一个固定长度的队列来缓存待执行的任务。当线程池中的工作线程已经全部占用时,新任务将会被放入队列末尾等待执行。由于队列的长度是有限的,所以有界队列策略可以控制线程池的负载,当任务的提交速度超过队列的处理能力时,新任务将会被拒绝并抛出RejectedExecutionException异常。

这三种任务排队策略各有优缺点,可以根据实际需求选择合适的策略。

拒绝策略(Rejection policy)

Java线程池中的拒绝策略是指当线程池中的工作队列已经满了,且线程池中的工作线程也已经全部占用时,新提交的任务该如何处理的策略。

四种拒绝策略

ThreadPoolExecutor预定义了四种拒绝策略:

  1. ThreadPoolExecutor.AbortPolicy:线程池默认的拒绝策略,当线程池已经达到最大线程数,并且工作队列也已经满了,新提交的任务将被立即拒绝并抛出RejectedExecutionException异常。
  2. ThreadPoolExecutor.CallerRunsPolicy:一种比较特殊的拒绝策略,当线程池已经达到最大线程数,并且工作队列也已经满了,新提交的任务将会被提交者所在的线程执行。这种策略可以确保新提交的任务一定会被执行,但是如果提交任务的线程也处于高负载状态时,可能会导致性能下降。
  3. ThreadPoolExecutor.DiscardPolicy:一种比较激进的拒绝策略,当线程池已经达到最大线程数,并且工作队列也已经满了,新提交的任务将会被直接丢弃,不会有任何异常抛出。
  4. ThreadPoolExecutor.DiscardOldestPolicy:一种比较温和的拒绝策略,当线程池已经达到最大线程数,并且工作队列也已经满了,新提交的任务将会替换掉队列中最早的任务。虽然这种策略可以避免新提交的任务被直接丢弃,但是替换掉最早的任务可能会导致某些任务无法执行。

也可以通过实现RejectedExecutionHandler接口,并重写rejectedExecution() 方法来自定义拒绝策略。

通俗解释

关于上面的参数我试着通俗的说一下,希望我说的能让你明白。
假如现在有一家外包公司(ThreadPoolExecutor),公司的核心开发(corePoolSize)有5个人,公司最多容纳(maximumPoolSize)10个开发,现在公司接了一个项目,核心开发还忙的过来,就将这个项目给其中一个核心开发做,慢慢的公司接的项目越来越多,5个核心开发都在做项目没时间再做新的项目,公司为了赚更多的钱新来的项目只能先接过来暂时积压(BlockingQueue)起来,但是一直积压也不是个事情,客户也会一直催,公司顶住最多只能积压5个,积压到5个之后公司也还能容纳5个开发,不得不再招人处理新的项目。当公司发展的越来越好,接的项目也越来越多这10个开发也忙不过来了,有新的项目再进来就只能通过各种方式拒绝(RejectedExecutionHandler)了。再后来因为疫情原因,公司能接到的项目也越来越少了,开发人员(Thread)很多已经没事儿可做了,大概过了两周时间(keepAliveTime),公司又为了节省开支就把这些空闲下来的非核心开发给开了。当然,核心开发也不是说一定不能动也是可以开的(allowCoreThreadTimeOut(true)),只不过肯定是优先考虑非核心人员。

有人说了,项目多的时候为啥不扩大公司规模呢?
首先,公司老板最多也就有养这几个员工的的能力,养的多了老板也吃不消,多招一个人可能也不会使工作效率提高,反而可能拖累其他开发的进度,能养几个员工也是经过老板深思熟虑加以往的经验总结得出的结果。

线程池大致流程图

线程池关闭

线程池的关闭操作分为两种方式:正常关闭强制关闭

  • 正常关闭
    正常关闭是通过ThreadPoolExecutor的shutdown()方法来实现的,它会依次关闭线程池中的各个线程,并等待它们执行完任务后再退出。

具体来说,正常关闭线程池需要执行以下步骤:

  1. 调用ThreadPoolExecutor的shutdown()方法,该方法会停止线程池中的任务添加,并尝试关闭所有的线程。
  2. 线程池中的所有线程会完成正在执行的任务,并尝试获取新的任务,但由于已经调用了shutdown()方法,所以不会再有新的任务被添加到线程池中。
  3. 一旦线程池中的所有线程都执行完了任务,并且等待队列中也没有任务了,线程池就会完全关闭,shutdown()方法才会返回。
  • 强制关闭
    强制关闭是通过ThreadPoolExecutor的shutdownNow()方法来实现的,它会立即关闭线程池中的所有线程,无论它们是否完成任务。

具体来说,强制关闭线程池需要执行以下步骤:

  1. 调用ThreadPoolExecutor的shutdownNow()方法,该方法会尝试关闭线程池中的所有线程。
  2. 线程池中的所有线程都会被中断,不管它们是否在执行任务。
  3. 等待队列中的所有任务也会被清除,并返回一个包含所有未执行的任务的列表。

需要注意的是,在关闭线程池之前,应该先调用boolean awaitTermination(long timeout, TimeUnit unit)方法等待线程池中的任务执行完毕,否则可能会有未执行的任务丢失,这样,我们可以确保线程池中的任务都被执行完毕,并且线程池中的资源都得到了正确的释放。

线程池优缺点

优点

  1. 提高系统性能:线程池可以限制线程的数量,减少了线程的创建和销毁所带来的开销,降低了系统资源的消耗,提高了系统的性能。
  2. 提高线程的可管理性:通过线程池可以有效地管理线程,统一管理和调度线程,使得线程的状态和行为更可控。
  3. 提高程序的可靠性:线程池可以有效地控制线程的并发数量,避免因过多线程导致的系统崩溃等问题。
  4. 提高代码的复用性:线程池可以让任务执行和线程分离,提高了代码的复用性。
  5. 提高响应速度:线程池可以在任务到达时立即开始执行,避免了线程创建和销毁的延迟,提高了响应速度

缺点

  1. 无法立即响应任务:线程池需要等待空闲线程才能执行新的任务,因此无法立即响应任务,对于需要实时响应的任务可能不适用。
  2. 线程池大小的设置需要合理:线程池的大小需要根据任务的类型、系统资源和并发数等因素综合考虑,如果设置不当可能会影响系统的性能和稳定性。
  3. 对于长时间的阻塞任务,可能会影响线程池的性能:如果线程池中的某个线程长时间被阻塞,可能会导致整个线程池的性能下降。

能力一般,水平有限,如有错误,请多指出。
如果对你有用点个关注给个赞呗,更多文章可以关注一下我的微信公众号suncodernote

  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

索码理

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

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

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

打赏作者

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

抵扣说明:

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

余额充值