多线程系列:二、线程池在 Spring 中的使用与关闭

  在 上一篇 中,我们了解了进程、线程、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 分两步:

  1. threadPoolExecutor.shutdown() 或 threadPoolExecutor.shutdownNow() :通知关闭线程池中的线程

  2. 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 的呢?(以后再来补充。)


  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值