在业务代码中,特别是基于spring体系的代码中,均会使用线程池进行一些操作,比如异步处理消息,定时任务,以及一些需要与当前业务分离开的操作等。常规情况下,使用spring体系的TaskExecutor或者是自己定义ExecutorService,均可以正常地完成相应的操作。不论是定义一个spring bean,或者是使用 static Thread工具类均是能满足条件。

但是,如果需要正常地关闭spring容器时,这些线程池就不一定能够按照预期地关闭了。结果就是,当使用代码 context.close() 时,期望进程会正常地退出,但实际上进程并不会退出掉.原因就在于这些线程池中还在运行的线程。

本文描述了在基于spring boot的项目中,如何正确地配置线程池,以保证线程池能够正确的在整个spring容器周期内运行,并且在容器正常关闭时能够一并退出掉.

定义bean时添加destroyMethod方法或相应生命周期方法

设置线程池中线程为daemon

为每个线程池正确地命名及使用ThreadFactory

丢弃不再需要的周期性任务

监听ContextClosedEvent,触发额外操作

定义Bean时的Lifecycle定义

将线程池定义为spring bean,为保证在容器关闭时,线程池一并关闭,可以为其定义相应的关闭方法。以下为特定的bean的关闭方法

ThreadPoolTaskExecutor#destroy
ThreadPoolTaskExecutor#destroy
ScheduledThreadPoolExecutor#shutdown

针对 ConcurrentTaskExecutor 这种并没有实现关闭方法的bean,则需要保证其所使用 executor 能够被正常地关闭

在spring 体系,让一个bean可以接收Lifecycle管理,有多种方法,如下所示

@Bean(destroyMethod) //定义时
@PreDestroy //bean方法
DisposableBean //接口
AutoCloseable //接口
Lifecycle //接口
//有方法名为 shutdown

设置线程池中线程为Daemon

一般情况下,关闭线程池后,线程池会自行将其中的线程结束掉.但针对一些自己伪装或直接new Thread()的这种线程,则仍会阻塞进程关闭。

按照,java进程关闭判定方法,当只存在Daemon线程时,进程才会正常关闭。因此,这里建议这些非主要线程均设置为 daemon,即不会阻塞进程关闭.

Thread.setDaemon(true)

不过更方便的是使用ThreadFactory,其在 newThread 时,可以直接操作定义出来的Thread对象(如后面所示)

正确命名Thread

在使用线程池时,一般会接受 ThreadFactory 对象,来控制如何创建thread。在java自带的ExecutorService时,如果没有设置此参数,则会使用默认的 DefaultThreadFactory. 效果就是,你会在 线程栈列表中,看到一堆的 pool-x-thread-y,在实际使用 jstack时,根本看不清这些线程每个所属的组,以及具体作用。

这里建议使用 guava 工具类 ThreadFactoryBuilder 来构建,如下代码参考所示

ThreadFactory threadFactory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat("定义任务-%d").build()

此代码,一是定义所有创建出来的线程为 daemon,此外相应的线程name均为 指定前缀开始。在线程栈中可以很方便地查找和定位. 此外,在判断影响进程退出时,也可以很方便地判断出是否是相关的线程池存在问题(查看daemon属性及name).

丢弃不再可用周期性任务

一般情况下,使用 java 自带的 ScheduledThreadPoolExecutor, 调用 scheduleAtFixedRate 及 scheduleWithFixedDelay 均会将任务设置为周期性的(period)。在线程池关闭时,这些任务均可以直接被丢弃掉(默认情况下). 但如果使用 schedule 添加远期的任务时,线程池则会因为其不是 周期性任务而不会关闭所对应的线程

如 spring 体系中 TriggerTask(包括CronTask), 来进行定时调度的任务,其最终均是通过 schedule 来实现调度,并在单个任务完成之后,再次 schedule 下一次任务的方式来执行。这种方式会被认为并不是 period. 因此,使用此调度方式时,尽管容器关闭时,执行了 shutdown 方法,但相应底层的 ScheduledExecutorService 仍然不会成功关闭掉(尽管所有的状态均已经设置完)。最终效果就是,会看到一个已经处于shutdown状态的线程池,但线程仍然在运行(状态为 wait 任务)的情况.

为解决此方法,java 提供一个额外的设置参数 executeExistingDelayedTasksAfterShutdown, 此值默认为true,即 shutdown 之后,仍然执行。可以通过在定义线程池时将其设置为 false,即线程池关闭之后,不再运行这些延时任务.

监听ContextClosedEvent事件

针对在业务中自己构建的bean时,可以通过上面的方式进行相应的控制,如果是三方的代码。则需要在容器关闭时通过触发额外的操作,来显示地进行三方的代码。如,在代码中获取到三方的对象,主动调用其的close方法.

惟一需要作的就是监听到容器关闭事件,然后在回调代码中进行相应的操作。因此,只需要定义beqn,实现以下接口即可

CallbackObj implements ApplicationListener {
public void onApplicationEvent(ContextClosedEvent event) {}
}

当此对象声明为bean时,当容器关闭时,会触发相应的事件,并回调起相应的操作.

总结

上面的几个方面均是从仔细控制线程池的创建和销毁来进行描述。基本思路即是从对象管理,生命周期以及代码控制多个方面来处理.在实际项目中,业务使用方只需要使用相应的组件即可,但组件提供方需要正确地提供,才能保证基础架构的完整性.

作者: flym

I am flym,the master of the site:)查看flym的所有文章