Spring环境下如何正确关闭线程池

1,054 阅读2分钟

记录一下年初生产环境出现的发版问题

需求:项目每次发版都会报错,每次异常都是线程池中线程获取数据库连接失败,原因是项目停止的时候没有考虑到线程池中正在执行的线程导致的;

正确关闭线程池的关键是:shutdown+awaitTermination或者shutdownNow+awaitTermination;

  • shutdown:执行此方法之后,将会拒绝新任务提交到线程池,待执行的任务不会取消,正在执行的任务也不会取消,将会继续执行知道任务结束;
  • shutdownNow:执行此方法之后,将会拒绝新任务提交到线程池,取消待执行的任务,尝试取消执行中的任务;
  • awaitTermination:等待线程池中任务执行完毕的超时时间;

场景:项目中有一个调度任务定时去扫描策略生成动作,每次发版都会导致这个任务的线程池中线程获取不到数据库连接报错;

理想的停机步骤:

  • 切断上游流量入口,确保不再有流量进入到当前节点;
  • 向应用发送kill命令,在设定的时间内待应用正常关闭,若超时,则使用kill -9强制关闭;
  • 当JVM接收到kill命令,会唤起应用中所有的【shutdown hook】,等待所有的【shutdown hook】执行完毕即可正常关机,与此同时,应用会接着处理在途请求,以确保不会向客户端抛出链接终端异常,实现无感知发布;

其实这个问题已经解决了,项目中也注册一个【shutdown hook】,然后关闭线程池(shutdown),设置关闭的超时时间(awaitTermination)即可;

但是Spring启动的时候也注册了【shutdown hook】;

它的作用就是对容器管理的Bean进行回收,并销毁容器;

Spring的【shutdown hook】与线程池里的任务并发执行,有可能导致任务依赖的资源(比如数据库连接)被提前回收;

目前问题是:在切断流量后,能否让线程池先关闭,然后再执行Spring的【shutdown hook】,避免依赖被提前回收;

两种解决方案:

  • 监听Spring的ContextClosedEvent事件,在事件被触发时关闭线程池;
  • 实现Lifecycle接口,重写stop方法关闭线程池;

因为发生异常的线程池是由Spring管理的,我们直接注入,设置关闭时调用shutdown方法而不是调用shutdownNow,然后设置关闭的超时时间,调用destroy方法即可;

后续:项目第一次发版之后还是那个异常,然后等待十几分钟,再次发版,发现监听到了Spring的ContextClosedEvent事件,然后成功关闭了线程池,没有报错了;