Java多款线程池,总有一款适合你。
  7kpCuc5qo18B 2023年11月02日 34 0



线程池的选择

  • 一:故事背景
  • 二:线程池原理
  • 2.1 ThreadPoolExecutor的构造方法的七个参数
  • 2.1.1 必须参数
  • 2.1.2 可选参数
  • 2.2 ThreadPoolExecutor的策略
  • 2.3 线程池主要任务处理流程
  • 2.4 ThreadPoolExecutor 如何做到线程复用
  • 三:四种常见线程池
  • 3.1 newCachedThreadPool
  • 3.2 newFixedThreadPool
  • 3.3 newSingleThreadExecutor
  • 3.4 newScheduledThreadPool
  • 四:线程池如何实现参数的动态修改
  • 五:实际应用
  • 六:总结提升


一:故事背景

最近咋搞多线程的研究学习。本文会系统的告诉你,Java中的多种线程池,以及在项目中该如何去选择对应的线程池,提升项目的处理能力。
池化技术是程序设计中非常常见的一种思想,大家可以通过我的博客池化思想了解,什么是池化思想。
为什么我们要使用线程池而不是创建线程去执行任务呢?

  1. 复用已创建的线程,避免创建线程的时候耗费资源
  2. 对线程进行统一管理
  3. 控制并发的数量,不至于创建的线程过多,导致资源消耗过多,最终造成服务器崩溃。

二:线程池原理

想要了解线程池原理,就先从其类图开始

Java多款线程池,总有一款适合你。_线程池


Executor 是线程池的顶级接口,其定义了方法execute。其中ThreadPoolExecutor类是我们重点关注的类,让我们来看看其构造方法

2.1 ThreadPoolExecutor的构造方法的七个参数

ThreadPoolExecutor一共有七个参数,其中5个是必须的参数,2个值可选参数。

2.1.1 必须参数

  1. int corePoolSize:该线程池中核心线程数最大值
    核心线程会一直存在在线程池中,无论是否有需要待执行的任务
  2. int maximumPoolSize:该线程池中线程总数最大值 。
    该值等于核心线程数量 + 非核心线程数量。这两个值加起来决定了可以创建多少个非核心线程
  3. long keepAliveTime:非核心线程闲置超时时长。非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。
  4. TimeUnit unit:keepAliveTime的单位。TimeUnit是一个枚举类型 ,包括以下属性:
    NANOSECONDS : 1微毫秒 = 1微秒 / 1000 MICROSECONDS : 1微秒 = 1毫秒 / 1000 MILLISECONDS : 1毫秒 = 1秒 /1000 SECONDS : 秒 MINUTES : 分 HOURS : 小时 DAYS : 天
  5. BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象。
    常用的几个阻塞队列:
    LinkedBlockingQueue :链式阻塞队列
    ArrayBlockingQueue:数组阻塞队列
    SynchronousQueue:同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
    DelayQueue:延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。

2.1.2 可选参数

  1. ThreadFactory threadFactory 创建线程的工厂,用于批量创建线程,可以在创建线程的时候指定一些参数,列如 守护线程,线程优先级等等。
  2. RejectedExecutionHandler handler。如果阻塞队列满了,且线程数大于最大线程数的时候就会采用拒绝策略。拒绝策略一共有四种:
  • ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

这四种拒绝策略是我们设计线程池必须要考虑的部分。在使用时,要估计可能产生的并发数,会不会触发拒绝策略,如果触发了拒绝策略我们该如何处理,如何保证程序提供功能的完整性。例如针对默认拒绝处理策略,我们可以捕获对应异常,进行重试。或者选择丢弃任务,然后过一段时间批量执行未成功的任务。

2.2 ThreadPoolExecutor的策略

线程池本身也有一个调度线程,这个线程用于管理布控整个线程池的任务和事务。线程池也有自己的状态。在ThreadPoolExecutor内使用了一些final int常量变量表示了线程池的状态。

Java多款线程池,总有一款适合你。_线程池_02

  • 线程池创建后就处于RUNNING状态
  • 调用shutdown()方法后处于SHUTDOWN状态,线程池不能接受新的任务,清除空闲的worker,不会等待阻塞队列任务完成
  • 调用shutdownNow()方法后处于STOP状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize=0,阻塞队列的size也为0。
  • 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。接着会执行terminated()函数。
  • 线程池处在TIDYING状态时,执行完terminated()方法之后,就会由 TIDYING -> TERMINATED, 线程池被设置为TERMINATED状态。

2.3 线程池主要任务处理流程

线程池主要任务处理流程在其execute方法内体现,让我们看看其是如何处理线程任务的:

Java多款线程池,总有一款适合你。_java_03

2.4 ThreadPoolExecutor 如何做到线程复用

上文我们说到线程池可以用来复用已经创建的线程对象,那么它到底是怎么做的呢?

其实,ThreadPoolExecutor在创建线程的时候会将线程封装成工作线程 worker、然后放入工作线程组中,然后反复的从阻塞队列中去拿任务去执行。

addWorker方法:

Java多款线程池,总有一款适合你。_多线程_04


worker对象循环去阻塞队列获取任务:

Java多款线程池,总有一款适合你。_线程池_05


获得任务之后,不断的进行task.run 执行对应的任务。

在getTask中,如果是在核心线程上的话,任务将会卡在workQueue.take();方法上,线程不会结束,如果是非核心线程的话,非核心线程会workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ,如果超时还没有拿到,下一次循环判断compareAndDecrementWorkerCount就会返回null,Worker对象的run()方法循环体的判断为null,任务结束,然后线程被系统回收 。

Java多款线程池,总有一款适合你。_ThreadPool_06

三:四种常见线程池

上文我们已经讲到了ThreadPoolExecutor线程池,讲解了其内部的各个参数以及各个参数如何选择。Execuors提供了几个不同的静态方法进行线程池的创建,这几个线程池底层都是使用的ThreadPoolExecutor进行的实现。

3.1 newCachedThreadPool

Java多款线程池,总有一款适合你。_开发语言_07


newCachedThreadPool适合执行很多的短时间的任务,并且线程60s会进行回收,占用资源不多。此线程池的任务会先将任务添加到synchronousQueue队列。由于线程池很大,几乎不会触发拒绝策略。

3.2 newFixedThreadPool

Java多款线程池,总有一款适合你。_开发语言_08


newFixedThreadPool只创建核心线程,不创建非核心线程。就算没有任务,核心线程也会保存。而且由于LinkedBlockingQueue的默认大小是Integer.MAX_VALUE,几乎不会触发拒绝策略。

3.3 newSingleThreadExecutor

Java多款线程池,总有一款适合你。_多线程_09


只创建一个核心线程处理任务,如果这个核心线程不空闲,新来的任务就放入阻塞队列,所有的任务按照先来先执行的顺序进行。

3.4 newScheduledThreadPool

Java多款线程池,总有一款适合你。_多线程_10

这四种常见的线程池,基本就够用了,但是如果业务规模过大,则存在资源耗尽的风险,所以还时老老实实的使用ThreadPoolExecutor类,自己进行参数配置吧。

四:线程池如何实现参数的动态修改

由于系统的复杂性,我们往往可能需要动态的调成线程池的参数,ThreadPoolExecutor类提供了几个方法来进行线程池参数的设置:

Java多款线程池,总有一款适合你。_线程池_11


主要有两个思路进行设置:

1:利用Nacos,业务服务读取线程池的配置,获取相对应的线程池实例进行线程池参数的修改。

2:也可以扩展ThreadPoolExecutor,重写方法,监听线程池参数的个变化,动态的修改线程池的参数。

五:实际应用

我们的项目场景中,年终总结的部分,用到了多线程,批量的计算用户本年度的各种参与数据,计算好之后,放入数据库中,用户直接读取即可。参数是如下选择:

  • corePoolSize:线程核⼼参数选择了0
  • maximumPoolSize:最⼤线程数选择了cpu*2
  • keepAliveTime:⾮核⼼闲置线程存活时间直接置为60
  • unit:⾮核⼼线程保持存活的时间选择了 TimeUnit.SECONDS 秒
  • workQueue:线程池等待队列,使⽤ LinkedBlockingQueue阻塞队列

由于我们是通过job任务进行触发,选择的是晚上用户少的时候进行的执行,并且只有晚上才会进行一次计算,所以并不需要保留核心线程占用程序客供件,只需要在任务处理时增加计算效率即可。

六:总结提升

多线程无疑会提升我们程序的效率,但是其参数选择非常重要,必须要结合线程池清楚ThreadPoolExecutor的7个参数,合理选择参数才能够安全使用。希望本篇文章能增加你对多线程的理解。


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  2Vtxr3XfwhHq   2024年05月17日   53   0   0 Java
  Tnh5bgG19sRf   2024年05月20日   107   0   0 Java
  8s1LUHPryisj   2024年05月17日   46   0   0 Java
  aRSRdgycpgWt   2024年05月17日   47   0   0 Java
7kpCuc5qo18B
作者其他文章 更多