前言
场景:当需要进行耗时且无状态的业务操作时,就需要用到子线程,用到子线程时可以选择new一个子线程来操作,这样效率会很低。而且消耗内存,遇到高并发的场景,子线程创建后没有及时回收,会造成内存不够,服务器宕机,所以就需要引用线程池来解决问题,ThreadPoolTaskExecutor是Spring框架提供的线程池技术。底层是基于JDK的ThreadPoolExecutor实现。
1.ThreadPoolTaskExecutor参数详解
2.参数分析与execute源码解析
3.拒绝策略详解
4.问题重现与分析
- ThreadPoolTaskExecutor参数详解
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活 -->
<!-- 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭 -->
<property name="corePoolSize" value="5"/>
<!-- 允许的空闲时间,当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize -->
<!-- 如果allowCoreThreadTimeout=true,则会直到线程数量=0 -->
<property name="keepAliveSeconds" value="200"/>
<!-- 线程池维护线程的最大数量 -->
<!-- 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务 -->
<!-- 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常,异常见下文 -->
<property name="maxPoolSize" value="10"/>
<!-- 缓存队列(阻塞队列)当核心线程数达到最大时,新任务会放在队列中排队等待执行 -->
<property name="queueCapacity" value="20"/>
<!-- 对拒绝task的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$AbortPolicy" />
</property>
</bean>
- 参数分析与execute源码解析
执行任务时,通过下面源代码可以得出以下结论
- 如果线程池中线程数量 < 核心线程数,新建一个线程执行任务;
- 如果线程池中线程数量 >= 核心线程数,则将任务放入任务队列
- 如果线程池中线程数量 >= 核心线程数 且 < maxPoolSize,且任务队列满了,则创建新的线程;
- 如果线程池中线程数量 > 核心线程数,当线程空闲时间超过了keepalive时,则会销毁线程;由此可见线程池的队列如果是无界队列,那么设置线程池最大数量是无效的;
- 如果线程池中的任务队列满了,而且线程数达到了maxPoolSize,并且没有空闲的线程可以执行新的任务,这时候再提交任务就会执行拒绝策略
//添加新任务
public void execute(Runnable command) {
//如果任务为null直接抛出异常
if (command == null)
throw new NullPointerException();
//ctl.get()它记录了当前线程池的运行状态和线程池内的线程数;一个变量是怎么记录两个值的呢?
//它是一个AtomicInteger 类型,有32个字节,这个32个字节中,高3位用来标识线程池的运行状态,
//低29位用来标识线程池内当前存在的线程数;
int c = ctl.get();
//如果当前线程数小于核心线程数,这时候任务不会进入任务队列,会创建新的工作线程直接执行任务;
if (workerCountOf(c) < corePoolSize) {
//添加新的工作线程执行任务,addWorker方法后面分析
if (addWorker(command, true))
return;
//addWorker操作返回false,说明添加新的工作线程失败,则获取当前线程池状态;(线程池数量小于corePoolSize情况下,
//创建新的工作线程失败,是因为线程池的状态发生了改变,已经处于非Running状态,或shutdown状态且任务队列为空)
c = ctl.get();
}
//以下两种情况继续执行后面代码
//1.前面的判断中,线程池中线程数小于核心线程数,并且创建新的工作线程失败;
//2.前面的判断中,线程池中线程数大于等于核心线程数
//线程池处于RUNNING状态,说明线程池中线程已经>=corePoolSize,这时候要将任务放入队列中,等待执行;
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//再次检查线程池的状态,如果线程池状态变了,非RUNNING状态下不会接收新的任务,需要将任务移除,
//成功从队列中删除任务,则执行reject方法处理任务;
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)//如果线程池的状态没有改变,且池中无线程
// 两种情况进入以该分支
//1.线程池处于RUNNING状态,线程池中没有线程了,因为有新任务进入队列所以要创建工作线程(这时候新任务已经在队列
//中,所以下面创建worker线程时第一个参数,要执行的任务为null,只是创建一个新的工作线程并启动它,
//让它自己去队列中取任务执行)
//2.线程池处于非RUNNING状态但是任务移除失败,导致任务队列中仍然有任务,但是线程池中的线程数为0,
//则创建新的工作线程,处理队列中的任务;
addWorker(null, false);
// 两种情况执行下面分支:
// 1.非RUNNING状态拒绝新的任务,并且无法创建新的线程,则拒绝任务
// 2.线程池处于RUNNING状态,线程池线程数量已经大于等于coresize,任务就需要放入队列,如果任务入队失败,
//说明队列满了,则创建新的线程,创建成功则新线程继续执行任务,如果创建失败说明线程池中线程数已经超过
//maximumPoolSize,则拒绝任务
}else if (!addWorker(command, false))
reject(command);
}
-
拒绝策略详解
拒绝策略RejectedExecutionHandler分为以下5种 -
1.AbortPolicy
该策略是线程池的默认策略。使用该策略时,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
//不做任何处理,直接抛出异常
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
- 2.DiscardPolicy
这个策略和AbortPolicy的slient版本,如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
//就是一个空的方法
}
- 3.DiscardOldestPolicy
这个策略从字面上也很好理解,丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if(!e.isShutdown()) {
//移除队头元素
e.getQueue().poll();
//再尝试入队
e.execute(r);
}
}
- 4.CallerRunsPolicy
使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。就像是个急脾气的人,我等不到别人来做这件事就干脆自己干。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
//直接执行run方法
r.run();
}
}
- 5.自定义
如果以上策略都不符合业务场景,那么可以自己定义一个拒绝策略,只要实现RejectedExecutionHandler接口,并且实现rejectedExecution方法就可以了。具体的逻辑就在rejectedExecution方法里去定义就OK了。
public class MyRejectPolicy implements RejectedExecutionHandler{
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//Sender是我的Runnable类,里面有message字段
if (r instanceof Sender) {
Sender sender = (Sender) r;
//直接打印
System.out.println(sender.getMessage());
}
}
}
这几种策略没有好坏之分,只是适用不同场景,具体哪种合适根据具体场景和业务需要选择,如果需要特殊处理就自己定义好了。
- 问题重现与分析
org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@6e55f339
[Running, pool size = 50, active threads = 50, queued tasks = 20, completed tasks = 5017]] did not accept task:
问题描述:因缓存队列长度太小导致的问题
上文提到当线程数=maxPoolSize达到最大线程数量,且任务队列已满时,线程池会拒绝处理任务而抛出异常,异常如下
[Running, pool size = 50, active threads = 50, queued tasks = 20, completed tasks = 5017]
异常显示线程池正在运行中,线程池线程数达到50(达到配置的最大值),活动线程50(没有空闲),队列存放20(配置最大值),完成5017.还有任务要插入队列时,队列满载,报出异常。但线程池还是在运行中的。
问题解决:
根据服务器配置,业务的情况来决定增加队列还是最大线程数(因为没有参考依据无法给出实际的参数配置)
写本文之前,也是生产环境报出上述异常,我通过增加队列长度来解决问题,因为做的是导出业务,是相对于耗时较长,无法增加最大线程数去解决,增大线程数的方式有内存压力过大的风险,可以根据业务,适量的增大队列长度。
文章为学习记录,有误敬请指出
参考资料:https://blog.csdn.net/zqz_zqz/article/details/69488570