1

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

 7 months ago
source link: https://blog.51cto.com/u_15854472/7871199
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

线程池的选择

  • 一:故事背景
  • 二:线程池原理
  • 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多款线程池,总有一款适合你。_多线程_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多款线程池,总有一款适合你。_多线程_06

三:四种常见线程池

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

3.1 newCachedThreadPool

Java多款线程池,总有一款适合你。_ThreadPool_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多款线程池,总有一款适合你。_java_11

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

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

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

五:实际应用

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

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

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

六:总结提升

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK