记两个有关线程池的小问题

最近小伙伴们找我查的问题里,有两个与线程池相关的,最终都是花了一些时间才揪出原因所在,做一下记录,供以后的自己和其它需要的人参考。

一、异步变同步

现象:

有一个方法,被请求后只是向线程池提交一个任务,然后马上返回,但从日志的 traceId 来看,偶现方法与任务在同一线程执行,接口耗时较长的情况。

分析过程:

这个其实就是一个知识点:当线程池里没有空闲线程,且任务队列已满时,会怎么处理新提交的任务?

可以看下 TheadPoolExecutor 类,这个类里面有几种预定义好的策略(implements RejectedExecutionHandler):

  • CallerRunsPolicy
  • AbortPolicy
  • DiscardPolicy
  • DiscardOldestPolicy

结合它们的名字以及注释就可以看到,它们分别对应:

  • 调度线程自己执行任务;(有一种例外情况是线程池被 shutdown 了则丢弃任务)
  • 忽略任务,并抛出异常;(默认值)
  • 丢弃任务,不产生异常;
  • 丢弃队列里最老的未被处理的任务,然后重新尝试调度新任务;(例外情况同一)

除此之外,还可以按需自己定义策略。

在我们的场景里,这个线程池使用的 RejectedExecutionHandler 是 CallerRunsPolicy,所以原因就找到了。

解决方案:

因为场景里主要的诉求是这个接口要快速返回,并且不能丢失任务,那这种情况使用消息队列会更加合适,所以将这里的向线程池提交任务,修改为向消息队列发送消息。

二、消失的任务

现象:

从日志可以看到,向线程池里提交了一个任务,找不到该任务执行的记录。

分析过程:

首先是怀疑这个任务被丢弃或者忽略了,经确认,该线程池的 RejectExecutionHandler 是使用的默认的 AbortPolicy,这样的话如果它被忽略,会有异常抛出,但日志里找不到异常记录。

那就是说,它成功进入了任务队列,但是没有被执行,哪里去了呢?

冥思苦想之后,怀疑是不是应用被杀掉了?查看 K8s 控制台里容器的滚动记录,果然在提交任务的时间点附近,应用发过版——破案。

解决方案:

提供两个思路:

  • 在保证任务执行逻辑幂等的前提下,通过消息队列、数据库记录任务状态+重试机制等方式调度任务;
  • 容器优雅下线,确认正在处理的请求和任务都完成后才能被 kill 掉。