一、为什么要用线程池
降低资源消耗。通过重复利用已创建的线程降低线程创建、销毁线程造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
线程池的运行原理
核心线程(corePool):线程池最终执行任务的角色肯定还是线程,同时我们也会限制线程的数量,所以我们可以这样理解核心线程,有新任务提交时,首先检查核心线程数,如果核心线程都在工作,而且数量也已经达到最大核心线程数,则不会继续新建核心线程,而会将任务放入等待队列。
等待队列 (workQueue):等待队列用于存储当核心线程都在忙时,继续新增的任务,核心线程在执行完当前任务后,也会去等待队列拉取任务继续执行,这个队列一般是一个线程安全的阻塞队列,它的容量也可以由开发者根据业务来定制。
非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,那么核心线程和非核心线程到底有什么区别呢?说出来你可能不信,本质上它们没有什么区别,创建出来的线程也根本没有标识去区分它们是核心还是非核心的,线程池只会去判断已有的线程数(包括核心和非核心)去跟核心线程数和最大线程数比较,来决定下一步的策略。
线程活动保持时间 (keepAliveTime):线程空闲下来之后,保持存货的持续时间,超过这个时间还没有任务执行,该工作线程结束。
饱和策略 (RejectedExecutionHandler):当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是抛弃要加入的任务。
关于线程池的状态,有5种,
RUNNING, 运行状态,值也是最小的,刚创建的线程池就是此状态。
SHUTDOWN,停工状态,不再接收新任务,已经接收的会继续执行
STOP,停止状态,不再接收新任务,已经接收正在执行的,也会中断
清空状态,所有任务都停止了,工作的线程也全部结束了
TERMINATED,终止状态,线程池已销毁。
二、ThreadPoolExecutor线程池类七大参数详解
参数 | 说明 |
---|---|
corePoolSize | 核心线程数量,线程池维护线程的最少数量 |
maximumPoolSize | 线程池维护线程的最大数量 |
keepAliveTime | 线程池除核心线程外的其他线程的最长空闲时间,超过该时间的空闲线程会被销毁 |
unit | keepAliveTime的单位,TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS |
workQueue | 线程池所使用的任务缓冲队列 |
threadFactory | 线程工厂,用于创建线程,一般用默认的即可 |
handler | 线程池对拒绝任务的处理策略 |
当线程池任务处理不过来的时候,可以通过handler指定的策略进行处理,
ThreadPoolExecutor提供了四种策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常;也是默认的处理方式。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
可以通过实现RejectedExecutionHandler接口自定义处理方式。
三、线程池任务执行
- 添加执行任务
submit() 该方法返回一个Future对象,可执行带返回值的线程;或者执行想随时可以取消的线程。Future对象的get()方法获取返回值。Future对象的cancel(true/false)取消任务,未开始或已完成返回false,参数表示是否中断执行中的线程
execute() 没有返回值。 - 线程池任务提交过程
2.1. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
2.2. 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
2.3. 如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
2.4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
2.5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
总结即:处理任务判断的优先级为 核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
-
线程池关闭
3.1. shutdown() 不接收新任务,会处理已添加任务
3.2. shutdownNow() 不接受新任务,不处理已添加任务,中断正在处理的任务 -
常用队列介绍
4.1. ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列.
4.2. SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。
4.3. LinkedBlockingQueue: 这是一个由单链表实现的默认×××的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。
队列操作:
方法 | 说明 |
---|---|
add | 增加一个元索; 如果队列已满,则抛出一个异常 |
remove | 移除并返回队列头部的元素; 如果队列为空,则抛出一个异常 |
offer | 添加一个元素并返回true; 如果队列已满,则返回false |
poll | 移除并返回队列头部的元素; 如果队列为空,则返回null |
put | 添加一个元素; 如果队列满,则阻塞 |
take | 移除并返回队列头部的元素; 如果队列为空,则阻塞 |
element | 返回队列头部的元素; 如果队列为空,则抛出一个异常 |
peek | 返回队列头部的元素; 如果队列为空,则返回null |
-
Executors线程工厂类`
-
Executors.newCachedThreadPool();
说明: 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
内部实现:new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue()); -
Executors.newFixedThreadPool(int);
说明: 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
内部实现:new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue()); -
Executors.newSingleThreadExecutor();
说明:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行。
内部实现:new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue()) -
Executors.newScheduledThreadPool(int);
说明:创建一个定长线程池,支持定时及周期性任务执行。
内部实现:new ScheduledThreadPoolExecutor(corePoolSize)
-
-
总结
ThreadPoolExecutor通过几个核心参数来定义不同类型的线程池,适用于不同的使用场景;其中在任务提交时,会依次判断corePoolSize, workQueque, 及maximumPoolSize,不同的状态不同的处理。
四、核心参数的配置依据
1.1、核心线程数量corePoolSize
核心线程数的设计需要根据任务的处理时间和每秒产生的任务数量来确定,例如执行一个任务需要0.1秒,系统百分之八十的时间没秒都会产生100个任务,那么我们想要在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数量为10,当时实际情况不可能这么平均,所以一般我们按照2080原则设计即可,即按照百分之80的情况设计核心线程数量,剩下的百分之20可以利用最大线程数量处理。
1.2、任务队列长度(workQueue)
任务队列长度一般设计为核心线程数/单个任务执行时间*2(任务最大等待时间/s)即可,例如上面场景中,核心线程数设计为10,单个任务执行时间为0.1,则队列长度可以设计为200
1.3、最大线程数(maximumPoolSize)
最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定,例如,上述环境中,如果系统每秒最大产生的任务数量是1000个,那么最大线程数=(最大任务数-任务队列长度)* 单个任务执行时间;既最大线程数=(1000-200)* 0.1 =80;当然最大线程数和服务器的硬件配置也有很大关系
五、如何合理的配置线程池的大小
一般需要根据任务的类型来配置线程池大小:
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
如果是IO密集型任务,参考值可以设置为2*NCPU
建议使用有界队列。因为有界队列能够增加系统的稳定性和预警的能力,我们可以想象一下,当我们使用无界队列的时候,当我们系统的后台的线程池的队列和线程池会越来越多,这样当达到一定的程度的时候,有可能会撑满内存,导致系统出现问题。当我们是有界队列的时候,当我们系统的后台的线程池的队列和线程池满了之后,会不断的抛出异常的任务,我们可以通过异常信息做一些事情。
当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。
六、线程池-四种拒绝策略
1、线程池的拒绝策略
线程池中,有三个重要的参数,决定影响了拒绝策略:
-
corePoolSize - 核心线程数,也即最小的线程数。
-
workQueue - 阻塞队列 。
-
maximumPoolSize - 最大线程数
当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到 maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。
2、拒绝策略定义
拒绝策略提供顶级接口 RejectedExecutionHandler ,其中方法 rejectedExecution 即定制具体的拒绝策略的执行逻辑。
jdk默认提供了四种拒绝策略:
- CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
- AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
- DiscardPolicy - 直接丢弃,其他啥都没有
- DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
3、测试代码
1、AbortPolicy
public static void main(String[] args) throws Exception{
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 5;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(10);
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, handler);
for(int i=0; i<100; i++) {
try {
executor.execute(new Thread(() -> log.info(Thread.currentThread().getName() + " is running")));
} catch (Exception e) {
log.error(e.getMessage(),e);
}
}
executor.shutdown();
}
executor.execute()提交任务,由于会抛出 RuntimeException,如果没有try.catch处理异常信息的话,会中断调用者的处理流程,后续任务得不到执行(跑不完100个)。可自行测试下,很容易在控制台console中能查看到。
2、CallerRunsPolicy
主体代码同上,更换拒绝策略:
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
运行后,在控制台console中能够看到的是,会有一部分的数据打印,显示的是 “main is running”,也即体现调用线程处理。
3、DiscardPolicy
更换拒绝策略
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
直接丢弃任务,实际运行中,打印出的信息不会有100条。
4、DiscardOldestPolicy
同样的,更换拒绝策略:
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
实际运行,打印出的信息也会少于100条。
4、总结
四种拒绝策略是相互独立无关的,选择何种策略去执行,还得结合具体的业务场景。实际工作中,一般直接使用 ExecutorService 的时候,都是使用的默认的 defaultHandler ,也即 AbortPolicy 策略。