ThreadPoolTaskExecutor参数详解、问题

前言

场景:当需要进行耗时且无状态的业务操作时,就需要用到子线程,用到子线程时可以选择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源码解析
    执行任务时,通过下面源代码可以得出以下结论
  1. 如果线程池中线程数量 < 核心线程数,新建一个线程执行任务;
  2. 如果线程池中线程数量 >= 核心线程数,则将任务放入任务队列
  3. 如果线程池中线程数量 >= 核心线程数 且 < maxPoolSize,且任务队列满了,则创建新的线程;
  4. 如果线程池中线程数量 > 核心线程数,当线程空闲时间超过了keepalive时,则会销毁线程;由此可见线程池的队列如果是无界队列,那么设置线程池最大数量是无效的;
  5. 如果线程池中的任务队列满了,而且线程数达到了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

  • 9
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值