• 问题现象

  • 原因分析

  • 任务调度逻辑

  • 汇总分析

  • 解决方案


问题现象

在我们的系统中,使用了这样的配置来开启异步操作:

spring配置

<task:annotation-driven executor="executor"

    scheduler="scheduler" />

<task:executor id="executor" pool-size="16-128"

    keep-alive="60" rejection-policy="CALLER_RUNS" queue-capacity="1000" />

客户端开启异步代码

@Async()

public Future<Result4Calculate> calculateByLendId(intdid) {

    // 标记1

    // 调用REST服务;监控调用时间。

}

获取Future后的处理

try {

    keplerOverdue = summay4Overdue.get(5, TimeUnit.SECONDS);

    // 后续处理

catch (Exception e) {

    // 标记2

    // 异常报警

}

然而在这种配置下,客户端在标记1处监控到的调用时间普遍在4s以内(平均时间不到1s,个别峰值请求会突破5s,全天超过5s的请求不到10个)。然而,在标记2处捕获到的超时异常却非常多(一天接近700+)。

问题出在哪儿?


原因分析

上述问题相关代码的调用时序如下图所示。

https://www.processon.com/view/link/585d381ee4b02e6c0ac86d66

其中,rest client 与rest server间的交互时间可以明确监控到,用时超过5s的非常少。但是,get方法却经常抛出超时异常。经过初步分析,问题出现在ThreadPoolTaskExecutor的任务调度过程中。


任务调度逻辑

使用<task:executor>注解得到的bean是ThreadPoolTaskExecutor的实例。这个类本身并不做调度,而是将调度工作委托给了ThreadPoolExecutor。后者的任务调度代码如下:

ThreadPoolExecutor任务调度代码

/**

 * Executes the given task sometime in the future.  The task

 * may execute in a new thread or in an existing pooled thread.

 *

 * If the task cannot be submitted for execution, either because this

 * executor has been shutdown or because its capacity has been reached,

 * the task is handled by the current {@code RejectedExecutionHandler}.

 *

 * @param command the task to execute

 * @throws RejectedExecutionException at discretion of

 *         {@code RejectedExecutionHandler}, if the task

 *         cannot be accepted for execution

 * @throws NullPointerException if {@code command} is null

 */

public void execute(Runnable command) {

    if (command == null)

        throw new NullPointerException();

    /*

     * Proceed in 3 steps:

     *

     * 1. If fewer than corePoolSize threads are running, try to

     * start a new thread with the given command as its first

     * task.  The call to addWorker atomically checks runState and

     * workerCount, and so prevents false alarms that would add

     * threads when it shouldn't, by returning false.

     *

     * 2. If a task can be successfully queued, then we still need

     * to double-check whether we should have added a thread

     * (because existing ones died since last checking) or that

     * the pool shut down since entry into this method. So we

     * recheck state and if necessary roll back the enqueuing if

     * stopped, or start a new thread if there are none.

     *

     * 3. If we cannot queue task, then we try to add a new

     * thread.  If it fails, we know we are shut down or saturated

     * and so reject the task.

     */

    int c = ctl.get();

    if (workerCountOf(c) < corePoolSize) {

        if (addWorker(command, true))

            return;

        c = ctl.get();

    }

    if (isRunning(c) && workQueue.offer(command)) {

        int recheck = ctl.get();

        if (! isRunning(recheck) && remove(command))

            reject(command);

        else if (workerCountOf(recheck) == 0)

            addWorker(nullfalse);

    }

    else if (!addWorker(command, false))

        reject(command);

}

通过其中的注释,我们可以知道它的核心调度逻辑如下(省略了一些检查等方法):

  1. 如果正在运行的线程数量小于corePoolSize(最小线程数),则尝试启动一个新线程,并将当入参command作为该线程的第一个task。否则进入步骤二。

  2. 如果没有按步骤1执行,那么尝试把入参command放入workQueue中。如果能成功入队,做后续处理;否则,进入步骤三。

  3. 如果没有按步骤2执行,那么将尝试创建一个新线程,然后做后续处理。

简单的说,当向ThreadPoolExecutor提交一个任务时,它会优先交给线程池中的现有线程;如果暂时没有可用的线程,那么它会将任务放到队列中;一般只有在队列满了的时候(导致无法成功入队),才会创建新线程来处理队列中的任务。

顺带一说,任务入队后,在某些条件下也会创建新线程。但新线程不会立即执行当前任务,而是从队列中获取一个任务并开始执行。


汇总分析

综上所述,我们可以确定以下信息:

  1. 根据系统配置,ThreadPoolExecutor中的corePoolSize = 16。

  2. 当并发数超过16时,ThreadPoolExecutor会按照步骤二进行任务调度,即把任务放入队列中,但没有及时创建新线程来执行这个任务

    这一点是推测。但同时,通过日志中的线程名称确认的线程池内线程数量没有增长。日志中,异步线程的id从executor-1、executor-2一直到executor-16,但17及以上的都没有出现过。

  3. 队列中的任务出现积压、时间累积,导致某一个任务超时后,后续大量任务都超时。但是超时并没有阻止任务执行;任务仍然会继续通过rest client调用rest server,并被监控代码记录下时间。
    任务在队列中积压、累积,是引发一天数百次异常、报警的原因。而监控代码并未监控到任务调度的时间,因此都没有出现超时。


解决方案

初步考虑方案有三:

  1. 提高初始线程数。
    提高步并发的初始线程数(如将16-128调整为32-128)。以此减少新任务进入队列的几率。
    但是这个方案只是降低队列积压的风险,并不解决问题。

  2. 关闭队列。
    将队列大小调整为0,以此保证每一个新任务都有一个新线程来执行。
    这个方案的问题在于,并发压力大时,可能导致线程不够用。此时的异步调用会根据rejection-policy="CALLER_RUNS"的配置而变为同步调用。

  3. 更换线程池。
    使用优先创建新线程(而非优先入队列)的线程池。
    改动最大的方案。

转载于:https://blog.51cto.com/winters1224/1885666

Spring中ThreadPoolTaskExecutor的线程调度及问题相关推荐

  1. spring中那些让你爱不释手的代码技巧

    紧接上文<spring中这些能升华代码的技巧,可能会让你爱不释手>.本文继续总结我认为spring中还不错的知识点,希望对您有所帮助. 一. @Conditional的强大之处 不知道你们 ...

  2. spring中这些能升华代码的技巧,可能会让你爱不释手

    前言 最近越来越多的读者认可我的文章,还是件挺让人高兴的事情.有些读者私信我说希望后面多分享spring方面的文章,这样能够在实际工作中派上用场.正好我对spring源码有过一定的研究,并结合我这几年 ...

  3. Spring5源码解析-Spring中的异步事件

    上一篇 Spring框架中的事件和监听器并未对Spring框架中的异步事件涉及太多,所以本篇是对其一个补充. 同步事件有一个主要缺点:它们在所调用线程的本地执行(也就是将所调用线程看成主线程的话,就是 ...

  4. java观察者模式在spring中的应用_利用spring自己实现观察者模式

    利用spring,自己实现的一个观察者模式,写着玩玩,目的是为了加深理解,下次用Spring自带的玩一玩. 首先我们定义一个侦听类接口 package com.hyenas.common.listen ...

  5. Spring 中那些让你爱不释手的代码技巧

    前言 最近越来越多的读者认可我的文章,还是件挺让人高兴的事情.有些读者私信我说希望后面多分享spring方面的文章,这样能够在实际工作中派上用场.正好我对spring源码有过一定的研究,并结合我这几年 ...

  6. 热乎乎的面经:Spring中Scheduled和Async两种调度方式有啥区别?

    最近有小伙伴出去面试,回来跟我说:冰河,我去XXX公司面试,面试官竟然问了我一个关于Spring中Scheduled和Async调度的问题,我竟然没回答上来,你能不能写一篇关于这个问题的文章呢?我:可 ...

  7. Spring中配置DataSource数据源的几种选择

    Spring中配置DataSource数据源的几种选择 在Spring框架中有如下3种获得DataSource对象的方法: 从JNDI获得DataSource. 从第三方的连接池获得DataSourc ...

  8. 详解设计模式在Spring中的应用

    设计模式作为工作学习中的枕边书,却时常处于勤说不用的尴尬境地,也不是我们时常忘记,只是一直没有记忆. 今天,螃蟹在IT学习者网站就设计模式的内在价值做一番探讨,并以spring为例进行讲解,只有领略了 ...

  9. Spring中利用applicationContext.xml文件实例化对象和调用方法

    Spring中实例化对象和调用方法入门 1.jar包和xml的准备 已上传至百度云盘,链接: https://pan.baidu.com/s/1CY0xQq3GLK06iX7tVLnp3Q 提取码: ...

最新文章

  1. 使用存储过程更新数据库!成功了但是返回值为 -1 的变态问题的解决办法!
  2. 数据结构与算法之贪心算法 C++实现
  3. 个人品牌的思考--《赢在中国》(2008-04-01)
  4. Linux常用的基本命令head、tail、tar、grep、date、cal(二)
  5. 【Python】 文件和操作文件方法
  6. class react 获取_「前端进阶」React系列九 - 受控非受控组件
  7. 解决dropbear在busybox中使用无法使用本地用户登录问题
  8. React-笔记整理
  9. SCQ16GS03M1F1C-32AA 紫光动态存储器
  10. hiho 1613 墨水滴 [Offer收割]编程练习赛32 Problem C 优先队列+BFS
  11. 计算机与代数---如何计算log---实现[2]
  12. 机器学习、数据挖掘、数据分析岗面试总结
  13. LOOPS HDU - 3853 dp求期望值
  14. 电源系列1:LDO 基本 原理(一)
  15. 对于VS2012的位图无法加载到资源视图“Bitmap”中的解决方案
  16. 李彦宏:未来五年语音图片搜索将超过文字
  17. python中plotly绘制树地图_聚类分析python画树状图--Plotly(dendrogram)用法解析 - 人人都是架构师...
  18. 关于 DevOps ,咱们聊的可能不是一回事
  19. 海康威视的设备怎么进行设置
  20. 计算机说课稿模板小学数学,优秀小学数学专用说课模板

热门文章

  1. Chrome v28 会在pwd目录下生成libpeerconnection.log文件
  2. poj 2828 Buy Tickets
  3. Winform窗体初始化Combox控件并模糊查找内容
  4. 编辑神器VIM下安装zencoding
  5. 日积月累真的很可怕,记住这些编程单词,两周学会敲代码
  6. mysql 绿色版远程访问_【Linux】MySQL解压版安装及允许远程访问
  7. 网络基础之HTTP协议
  8. springboot项目实例_Springboot项目的接口防刷(实例)
  9. 关于学习Python的一点学习总结(23->跳出循环)
  10. poj1410(线段相交问题判断)