SpringBoot 优雅关闭线程池

发布时间 2023-11-23 14:37:23作者: YangDanMua

参考:Spring环境中正确关闭线程池的姿势

参考:Spring Shutdown Hook工作机制揭秘

参考:线程池10:线程池的5种状态

JDK 线程池的三个方法

shutdown
将会拒绝新任务提交到线程池;待执行的任务不会取消,正在执行的任务也不会取消,将会继续执行直到结束

  • 拒绝新任务提交(改线程池状态)
  • 待执行的任务不会取消
  • 正在执行的任务也不会取消,将继续执行

shutdownNow
将会拒绝新任务提交到线程池;取消待执行的任务,尝试取消执行中的任务。

  • 拒绝新任务提交(改线程池状态)
  • 取消待执行的任务(任务队列移除即可)
  • 尝试取消执行中的任务(仅仅是做尝试,成功与否取决于是否响应InterruptedException,以及对其做出的反应)(中断线程)

awaitTermination
等待线程池关闭(直到TERMINATED状态)(有超时时间参数)

线程池状态变更

线程池状态

  • RUNNING:接收新任务并处理排队任务
  • SHUTDOWN:不接受新任务,但处理排队任务(调用 shutdown 后)
  • STOP:不接受新任务,不处理排队任务,中断正在进行的任务(shutdownNow后)
  • TIDYING:所有任务终止,wokerCount 为零,线程池转换到 TIDYING 状态,并运行 terminate 钩子方法
  • TERMINATED:terminate 运行完成

SpringBoot 的 destroy 销毁 Bean

在这里开始DefaultSingletonBeanRegistry#destroySingletons

这里涉及AbstractApplicationContext#doClose

protected void doClose() {
  // ContextClosedEvent 事件
  publishEvent(new ContextClosedEvent(this));
  // LifecycleProcessor
  this.lifecycleProcessor.onClose();
  // 销毁单例 Bean
  destroyBeans();
  // ...
}

SpringBoot 间接提供了 SpringApplication.getShutdownHandlers().add(),不过即便容器是 SpringApplicationShutdownHook 关闭的,handler 的执行也在容器 close 的后面。

那么一般需要自己监听 ContextClosedEvent 来保证线程池在依赖的 Bean 之前销毁 ?

支持 @Async 的线程池

ThreadPoolTaskExecutor

public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
  implements AsyncListenableTaskExecutor, SchedulingTaskExecutor

在抽象类 ExecutorConfigurationSupport 中继承了 DisposableBean,虽然继承了 DisposableBean,但是无法保证线程池在执行的任务依赖的 Bean 之后销毁,如其等待队列的任务依赖 Redis 连接池,但是 Redis 连接池可以在线程池之前销毁,因为 Spring 无法知道任务依赖。

public void destroy() {
  shutdown();
}

public void shutdown() {
  if (this.executor != null) {
    // 等待任务执行完毕, 默认 false
    if (this.waitForTasksToCompleteOnShutdown) {
      this.executor.shutdown();
    }
    else {
      // 取消等待执行的任务
      for (Runnable remainingTask : this.executor.shutdownNow()) {
        cancelRemainingTask(remainingTask);
      }
    }
    // 等待线程池关闭成功
    awaitTerminationIfNecessary(this.executor);
  }
}

但是好消息是 shutdown 和 shutdownNow 是可以被重复调用的,因此可以在 ContextClosedEvent 中 shutdown 一次,再在 DisposableBean 中 shutdown 一次。

Spring 和 SpringBoot 的 shudownHook

AbstractApplicationContext#registerShutdownHook,Spring 提供了接口调用让自己在 Runtime.getRuntime().addShutdownHook 中 doClose,但是 SpringBoot 似乎没有调用,而是使用自己的 SpringApplicationShutdownHook hook 自己,然后自己再调用容器的 doClose。

@Override
public void AbstractApplicationContext.registerShutdownHook() {
  if (this.shutdownHook == null) {
    // No shutdown hook registered yet.
    this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
      @Override
      public void run() {
        synchronized (startupShutdownMonitor) {
          doClose();
        }
      }
    };
    Runtime.getRuntime().addShutdownHook(this.shutdownHook);
  }
}
void SpringApplicationShutdownHook.addRuntimeShutdownHook() {
  try {
    Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
  }
  catch (AccessControlException ex) {
    // Not allowed in some environments
  }
}

@Override
public void run() {
  contexts.forEach(this::closeAndWait);
  // ContextClosedEvent 中已 Close 的再调用一遍
  closedContexts.forEach(this::closeAndWait);
  // Handlers
  actions.forEach(Runnable::run);
}

有没有方便的处理自定义线程池的方法

封装一遍

public class SmartThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
  public SmartThreadPoolTaskExecutor() {
    SmartThreadPoolTaskExecutorShutdownHook.addHook(this);
  }
}
@Configuration
@ConditionalOnBean(SmartThreadPoolTaskExecutor.class)
public class SmartThreadPoolTaskExecutorShutdownHook {

  private static final Set<ThreadPoolTaskExecutor> HOOK_LIST = new HashSet<>();

  public static void addHook(ThreadPoolTaskExecutor executor) {
    HOOK_LIST.add(executor);
  }

  @Bean
  public ContextClosedEventListener contextClosedEventListener(){
    return new ContextClosedEventListener();
  }


  public static class ContextClosedEventListener implements ApplicationListener<ContextClosedEvent> {

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
      HOOK_LIST.forEach(ExecutorConfigurationSupport::destroy);
    }
  }
}

这里有个问题,如果想通过 ThreadPoolTaskExecutor 创建 SmartThreadPoolTaskExecutor 怎么处理?
下面这种创建代理好吗?

public static SmartThreadPoolTaskExecutor warp(ThreadPoolTaskExecutor executor) {
  AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new SmartThreadPoolTaskExecutor());
  proxyFactory.setProxyTargetClass(true);
  proxyFactory.addAspect(new ExecutorAspect(executor));
  return proxyFactory.getProxy();
}

@Aspect
private static class ExecutorAspect {

  private final ThreadPoolTaskExecutor executor;

  public ExecutorAspect(ThreadPoolTaskExecutor executor) {
    this.executor = executor;
  }

  @Pointcut("execution(* org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.*(..))")
  public void pointcut() {
  }

  @Around("pointcut()")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    String name = pjp.getSignature().getName();
    Class<?>[] args = Arrays.stream(pjp.getArgs())
        .map(Object::getClass)
        .collect(Collectors.toList())
        .toArray(new Class<?>[]{});
    Method method = executor.getClass().getMethod(name, args.length == 0 ? null : args);
    return method.invoke(executor, pjp.getArgs());
  }
}

如果不想在创建实例时默认处理,也可以手动调用 SmartThreadPoolTaskExecutorShutdownHook#addHook

如果想继承的类的所有方法调用包括反射全部代理到父类实例该如何处理

线程池顺序处理
改 TreeSet,加上 @Order 注解和默认 order 字段处理。

public class SmartThreadPoolTaskExecutor extends ThreadPoolTaskExecutor implements Ordered {
  
  public static final int DEFAULT_ORDER = 0;

  private int order = DEFAULT_ORDER;

  public SmartThreadPoolTaskExecutor() {
    SmartThreadPoolTaskExecutorShutdownHook.addHook(this);
  }

  public SmartThreadPoolTaskExecutor(int order) {
    this.order = order;
    SmartThreadPoolTaskExecutorShutdownHook.addHook(this);
  }

  public SmartThreadPoolTaskExecutor(boolean hook) {
    if (hook) {
      SmartThreadPoolTaskExecutorShutdownHook.addHook(this);
    }
  }

  public SmartThreadPoolTaskExecutor(boolean hook, int order) {
    this.order = order;
    if (hook) {
      SmartThreadPoolTaskExecutorShutdownHook.addHook(this);
    }
  }
}
@Configuration
public class SmartThreadPoolTaskExecutorShutdownHook {

  private static final List<ThreadPoolTaskExecutor> HOOK_LIST = new ArrayList<>();

  public static void addHook(ThreadPoolTaskExecutor executor) {
    HOOK_LIST.add(executor);
  }

  @Bean
  public ContextClosedEventListener contextClosedEventListener() {
    return new ContextClosedEventListener();
  }


  public static class ContextClosedEventListener implements ApplicationListener<ContextClosedEvent> {

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
      List<ThreadPoolTaskExecutor> sortedList = HOOK_LIST.stream()
          .sorted(Comparator.comparingInt(SmartThreadPoolTaskExecutorShutdownHook::getOrder))
          .collect(Collectors.toList());
      sortedList.forEach(ExecutorConfigurationSupport::destroy);
    }
  }

  private static int getOrder(ThreadPoolTaskExecutor executor) {
    if(executor instanceof Ordered){
      return ((Ordered) executor).getOrder();
    }

    return SmartThreadPoolTaskExecutor.DEFAULT_ORDER;
  }
}

问题

像这种继承了某类,wrap 后想将所有桥接调用全部转发到 target 上如何实现呢(特别是 private、protected的,或者不用考虑这种情况?直接新建一个 Wrap 类,覆盖 public 方法)?
主要是 ThreadPoolTaskExecutor 有一个 TaskExecutorBuilder 构建器,一般需要兼容它们。