在 上一篇 中,我们了解了进程、线程、Java 线程池的一些基础概率,然后抛出了线程池在 Spring 中的使用与关闭两个问题,本篇将通过案例对这两个问题进行探讨。
ThreadPool 在 Spring 中的使用
1. 使用 JUC 下面的 ThreadPoolExecutor
Bean 默认为单例类型,所以只需要在 Spring IOC容器加载时,添加一个 Bean 即可
@Bean
public ExecutorService IOThreadPool() {
int processors = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(processors , processors *2 , 60L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(256));
}
2. 使用 Spring 提供的 ThreadPoolTaskExecutor 管理 ThreadPoolExecutor
ThreadPoolTaskExecutor 底层也是用 ExecutorService 去做的,但尽量使用这种方式,让 ThreadPoolTaskExecutor 自动去完成一些简单的、但又不得不做的逻辑。
包括其他的一些字符串、集合等操作,都尽量用成熟的工具去处理,在使用过后,我们也还是有必要去了解工具是怎么做的,做到我可以用“你”,但我不用“你”我也不会踩坑的境界。
@Bean
public ThreadPoolTaskExecutor cpuThreadPool() {
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor poolTaskExecutor=new ThreadPoolTaskExecutor();
//基础参数设置
poolTaskExecutor.setCorePoolSize(processors);
poolTaskExecutor.setMaxPoolSize(processors+1);
poolTaskExecutor.setKeepAliveSeconds(60);
poolTaskExecutor.setAllowCoreThreadTimeOut(false);
poolTaskExecutor.setQueueCapacity(256);
//开启在销毁线程池之前执行shutdown方法
poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
//设置通知关闭线程池后的等待时间
poolTaskExecutor.setAwaitTerminationSeconds(20);
return poolTaskExecutor;
}
思考:
在构建 ThreadPoolTaskExecutor 和 ThreadPoolExecutor 时,我们可以看到 ThreadPoolTaskExecutor 不需要手动设置任务队列的类型,源码如下:
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#createQueue
protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
if (queueCapacity > 0) {
return new LinkedBlockingQueue<>(queueCapacity);
}
else {
return new SynchronousQueue<>();
}
}
那么,上面这个流程创建的阻塞队列适合所有业务场景吗?
答案是可以的,但前提是要为业务选择合适的 queueCapacity 值,有兴趣可以看 LinkedBlockingQueue引发的一次线上事故
如何关闭 ThreadPool
关闭 ThreadPoolExecutor 分两步:
-
threadPoolExecutor.shutdown() 或 threadPoolExecutor.shutdownNow() :通知关闭线程池中的线程
-
threadPoolExecutor.awaitTermination(timeout,TimeUnit):等待关闭的时间
关闭 ThreadPool 时,如何处理各个阶段的任务呢?
首先,我们需要知道任务执行的几个阶段:
- 未提交,此时可以将任务提交到线程池
- 已提交未执行,此时任务已在线程池的阻塞队列中,等待着执行
- 执行中,此时任务正在执行
- 执行完毕
在执行 shutdown 或 shutdownNow 方法之后,将影响任务的执行
/**
* Initiates an orderly shutdown in which previously submitted
* tasks are executed, but no new tasks will be accepted.
* Invocation has no additional effect if already shut down.
*
* <p>This method does not wait for previously submitted tasks to
* complete execution. Use {@link #awaitTermination awaitTermination}
* to do that.
*
* @throws SecurityException {@inheritDoc}
*/
public void shutdown() {
...
将线程池设置为shutdown状态
使用 thread.interrupt 通知所有线程关闭
...
}
- 拒绝新任务提交
- 待执行的任务不会取消
- 正在执行的任务也不会取消,将继续执行
/**
* Attempts to stop all actively executing tasks, halts the
* processing of waiting tasks, and returns a list of the tasks
* that were awaiting execution. These tasks are drained (removed)
* from the task queue upon return from this method.
*
* <p>This method does not wait for actively executing tasks to
* terminate. Use {@link #awaitTermination awaitTermination} to
* do that.
*
* <p>There are no guarantees beyond best-effort attempts to stop
* processing actively executing tasks. This implementation
* cancels tasks via {@link Thread#interrupt}, so any task that
* fails to respond to interrupts may never terminate.
*
* @throws SecurityException {@inheritDoc}
*/
public List<Runnable> shutdownNow() {
...
将线程池设置为stop状态
使用 thread.interrupt 通知所有线程关闭
...
}
- 拒绝新任务提交
- 取消待执行的任务
- 尝试取消执行中的任务(仅仅是做尝试,成功与否取决于是否响应InterruptedException,以及对其做出的反应)
最后调用 awaitTermination ,等待 ThreadPool 关闭
shutdown() 和 shutdownNow() 方法的注释中都写道:This method does not wait for actively executing tasks to terminate. Use awaitTermination to do that.也就是说,这两个方法都是异步通知的。
/**
* Blocks until all tasks have completed execution after a shutdown
* request, or the timeout occurs, or the current thread is
* interrupted, whichever happens first.
*
* @param timeout the maximum time to wait
* @param unit the time unit of the timeout argument
* @return {@code true} if this executor terminated and
* {@code false} if the timeout elapsed before termination
* @throws InterruptedException if interrupted while waiting
*/
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
我们可以不使用 awaitTermination ,但要考虑后续的逻辑,会不会影响到 ThreadPool 中未完成任务的正常执行。
关闭 ThreadPoolTaskExecutor 只需一步:
ThreadPoolTaskExecutor.destroy();
destroy方法其实就是对 ThreadPoolExecutor 的关闭做了一次封装,具体的源码比较简单,总结一下就是:
- 初始化时,如果设置了 waitForTasksToCompleteOnShutdown = true ,等同 threadPoolExecutor.shutdown() + .awaitTermination
- 初始化时,如果没有设置或设置 waitForTasksToCompleteOnShutdown = false ,等同 threadPoolExecutor.shutdownNow() + .awaitTermination
如何在 Spring 应用中,正确的关闭 ThreadPool ?
当 JVM 收到 kill 指令后,便会唤醒所有的 Shutdown Hook,而其中有一个 Shutdown Hook 是 Spring 应用在启动之初注册的,它的作用是对 Spring 管理的 Bean 进行回收,并销毁 IOC 容器。
当在 Spring 中,关闭 ThreadPool 时,需要考虑 ThreadPool 中依赖 Bean 被回收的问题。例如:
应用中,有一个线程的工作是每隔 1s,将得到的数据集合,分批的存储到 DB 中。
当应用关闭时,Shutdown Hook 被触发,开始回收 IOC 中的 Bean,但此时如果线程池中使用的是被销毁的 Bean,那么数据入库失败,本次需要存储的数据丢失。
所以,我们需要在所有 Bean 销毁前,执行 ThreadPool 的关闭。
那么我们怎么提前得到 Bean 的销毁事件呢?
Spring 为我们准备了对应的接口,监听 ContextClosedEvent 事件,在 IOC 容器被销毁前,关闭线程池。
@Configuration
public class ThreadPoolConfig implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
//ExecutorService的关闭,没有被Spring监听到,所以手动告知Bean销毁前关闭ExecutorService
executorService.shutdown();
executorService.awaitTermination(3, TimeUnit.SECONDS);
}
}
但如果使用的是 Spring 提供的 ThreadPoolTaskExecutor,是不需要注册 ContextClosedEvent 或实现 LifecycleProcessor 接口的,Spring 也会等到线程池关闭(超时)后,再接着往下执行 Bean 的销毁、资源回收、应用上下文关闭的逻辑,确保被依赖资源不会被提前回收掉。
至此,我们已经可以在 Spring 中正常的关闭 ThreadPool 了,但 ContextClosedEvent、LifecycleProcessor 是在什么时候执行的呢?
线程池关闭的源码跟踪
通过追踪源码,发现 IOC 容器的销毁在 AbstractApplicationContext . doClose() 中执行。我们可以看到有两处地方,第一处就是我们上文采取的线程池关闭方式,第二处是 LifecycleProcessor.onClose()(我们可以重写 DefaultLifecycleProcessor 中的 onclose 方法,先关闭线程池,然后执行父类的 onclose 方法)。
我们在使用 ThreadPoolTaskExecutor 时,Spring 为我们自动执行了关闭,那么它是如何保证在所有依赖 Bean 被销毁前,关闭 ThreadPool 的呢?(以后再来补充。)