线程池的选择
- 一:故事背景
- 二:线程池原理
- 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中的多种线程池,以及在项目中该如何去选择对应的线程池,提升项目的处理能力。
池化技术是程序设计中非常常见的一种思想,大家可以通过我的博客池化思想了解,什么是池化思想。
为什么我们要使用线程池而不是创建线程去执行任务呢?
- 复用已创建的线程,避免创建线程的时候耗费资源
- 对线程进行统一管理
- 控制并发的数量,不至于创建的线程过多,导致资源消耗过多,最终造成服务器崩溃。
二:线程池原理
想要了解线程池原理,就先从其类图开始
Executor 是线程池的顶级接口,其定义了方法execute。其中ThreadPoolExecutor类是我们重点关注的类,让我们来看看其构造方法
2.1 ThreadPoolExecutor的构造方法的七个参数
ThreadPoolExecutor一共有七个参数,其中5个是必须的参数,2个值可选参数。
2.1.1 必须参数
-
int corePoolSize:该线程池中核心线程数最大值
核心线程会一直存在在线程池中,无论是否有需要待执行的任务 -
int maximumPoolSize:该线程池中线程总数最大值 。
该值等于核心线程数量 + 非核心线程数量。这两个值加起来决定了可以创建多少个非核心线程 -
long keepAliveTime:非核心线程闲置超时时长。非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。
-
TimeUnit unit:keepAliveTime的单位。TimeUnit是一个枚举类型 ,包括以下属性:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000 MICROSECONDS : 1微秒 = 1毫秒 / 1000 MILLISECONDS : 1毫秒 = 1秒 /1000 SECONDS : 秒 MINUTES : 分 HOURS : 小时 DAYS : 天 -
BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象。
常用的几个阻塞队列:
LinkedBlockingQueue :链式阻塞队列
ArrayBlockingQueue:数组阻塞队列
SynchronousQueue:同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
DelayQueue:延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。
2.1.2 可选参数
- ThreadFactory threadFactory 创建线程的工厂,用于批量创建线程,可以在创建线程的时候指定一些参数,列如 守护线程,线程优先级等等。
- RejectedExecutionHandler handler。如果阻塞队列满了,且线程数大于最大线程数的时候就会采用拒绝策略。拒绝策略一共有四种:
-
ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
-
ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
-
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
-
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
这四种拒绝策略是我们设计线程池必须要考虑的部分。在使用时,要估计可能产生的并发数,会不会触发拒绝策略,如果触发了拒绝策略我们该如何处理,如何保证程序提供功能的完整性。例如针对默认拒绝处理策略,我们可以捕获对应异常,进行重试。或者选择丢弃任务,然后过一段时间批量执行未成功的任务。
2.2 ThreadPoolExecutor的策略
线程池本身也有一个调度线程,这个线程用于管理布控整个线程池的任务和事务。线程池也有自己的状态。在ThreadPoolExecutor内使用了一些final int常量变量表示了线程池的状态。
- 线程池创建后就处于RUNNING状态
- 调用shutdown()方法后处于SHUTDOWN状态,线程池不能接受新的任务,清除空闲的worker,不会等待阻塞队列任务完成
- 调用shutdownNow()方法后处于STOP状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize=0,阻塞队列的size也为0。
- 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。接着会执行terminated()函数。
- 线程池处在TIDYING状态时,执行完terminated()方法之后,就会由 TIDYING -> TERMINATED, 线程池被设置为TERMINATED状态。
2.3 线程池主要任务处理流程
线程池主要任务处理流程在其execute方法内体现,让我们看看其是如何处理线程任务的:
2.4 ThreadPoolExecutor 如何做到线程复用
上文我们说到线程池可以用来复用已经创建的线程对象,那么它到底是怎么做的呢?
其实,ThreadPoolExecutor在创建线程的时候会将线程封装成工作线程 worker、然后放入工作线程组中,然后反复的从阻塞队列中去拿任务去执行。
addWorker方法:
worker对象循环去阻塞队列获取任务:
获得任务之后,不断的进行task.run 执行对应的任务。
在getTask中,如果是在核心线程上的话,任务将会卡在workQueue.take();方法上,线程不会结束,如果是非核心线程的话,非核心线程会workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ,如果超时还没有拿到,下一次循环判断compareAndDecrementWorkerCount就会返回null,Worker对象的run()方法循环体的判断为null,任务结束,然后线程被系统回收 。
三:四种常见线程池
上文我们已经讲到了ThreadPoolExecutor线程池,讲解了其内部的各个参数以及各个参数如何选择。Execuors提供了几个不同的静态方法进行线程池的创建,这几个线程池底层都是使用的ThreadPoolExecutor进行的实现。
3.1 newCachedThreadPool
newCachedThreadPool适合执行很多的短时间的任务,并且线程60s会进行回收,占用资源不多。此线程池的任务会先将任务添加到synchronousQueue队列。由于线程池很大,几乎不会触发拒绝策略。
3.2 newFixedThreadPool
newFixedThreadPool只创建核心线程,不创建非核心线程。就算没有任务,核心线程也会保存。而且由于LinkedBlockingQueue的默认大小是Integer.MAX_VALUE,几乎不会触发拒绝策略。
3.3 newSingleThreadExecutor
只创建一个核心线程处理任务,如果这个核心线程不空闲,新来的任务就放入阻塞队列,所有的任务按照先来先执行的顺序进行。
3.4 newScheduledThreadPool
这四种常见的线程池,基本就够用了,但是如果业务规模过大,则存在资源耗尽的风险,所以还时老老实实的使用ThreadPoolExecutor类,自己进行参数配置吧。
四:线程池如何实现参数的动态修改
由于系统的复杂性,我们往往可能需要动态的调成线程池的参数,ThreadPoolExecutor类提供了几个方法来进行线程池参数的设置:
主要有两个思路进行设置:
1:利用Nacos,业务服务读取线程池的配置,获取相对应的线程池实例进行线程池参数的修改。
2:也可以扩展ThreadPoolExecutor,重写方法,监听线程池参数的个变化,动态的修改线程池的参数。
五:实际应用
我们的项目场景中,年终总结的部分,用到了多线程,批量的计算用户本年度的各种参与数据,计算好之后,放入数据库中,用户直接读取即可。参数是如下选择:
- corePoolSize:线程核⼼参数选择了0
- maximumPoolSize:最⼤线程数选择了cpu*2
- keepAliveTime:⾮核⼼闲置线程存活时间直接置为60
- unit:⾮核⼼线程保持存活的时间选择了 TimeUnit.SECONDS 秒
- workQueue:线程池等待队列,使⽤ LinkedBlockingQueue阻塞队列
由于我们是通过job任务进行触发,选择的是晚上用户少的时候进行的执行,并且只有晚上才会进行一次计算,所以并不需要保留核心线程占用程序客供件,只需要在任务处理时增加计算效率即可。
六:总结提升
多线程无疑会提升我们程序的效率,但是其参数选择非常重要,必须要结合线程池清楚ThreadPoolExecutor的7个参数,合理选择参数才能够安全使用。希望本篇文章能增加你对多线程的理解。