Java并发编程-线程池优雅关闭(四)

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

前言

线程池我们平时项目中都会用到,也是面试过程中必问的知识点,但是我们很少注意到线程要如何做安全关闭,常常导致生产环境的一些问题,这些情况定位起来比较麻烦因为没有打印日志,我们可能统一归结为发布异常。 其实这一块的内容挺多,优雅发布,优雅停机…等待.

线程池系列

Java并发编程-线程池(一)
Java并发编程-线程池源码分析(二)
Java并发编程-JDK线程池和Spring线程池(三)

1. 案例

例子比较简单, 定义一个Spring线程池, 线程池里面task调用了一个订单的服务, 订单服务保存到缓存里面,然后打印。

1.1 线程池配置

@Configuration
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolTaskExecutor createThreadPoolTaskExecutor () {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("xx定时任务");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.initialize();
        return executor;
    }
}
复制代码

1.2 订单对象和订单服务

@Data
public class OrderDto {
    private String orderNo;
    private Long goodsId;
}

@Service
public class OrderService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    @SneakyThrows
    public void saveOrder() {
        OrderDto orderDto = new OrderDto();
        orderDto.setOrderNo("No2021081501");
        orderDto.setGoodsId(1L);
        System.out.println("开始保存订单信息:");
        stringRedisTemplate.opsForValue().set("No2021081501", JSON.toJSONString(orderDto));
        System.out.println("保存订单信息成功");
        String orderInfo = stringRedisTemplate.opsForValue().get("No2021081501");
        System.out.println("orderInfo:" + orderInfo);
    }
}

复制代码

1.3 单元测试

@Service
public class OrderService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public void saveOrder() {
        OrderDto orderDto = new OrderDto();
        orderDto.setOrderNo("No2021081501");
        orderDto.setGoodsId(1L);
        System.out.println("开始保存订单信息:");
        stringRedisTemplate.opsForValue().set("No2021081501", JSON.toJSONString(orderDto));
        System.out.println("保存订单信息成功");
        String orderInfo = stringRedisTemplate.opsForValue().get("No2021081501");
        System.out.println("orderInfo:" + orderInfo);
    }
}
复制代码

1.4 运行结果

image.png
到保存订单信息就断了,没有往下执行,也没有异常,按道理应该打印订单信息.

打个断点进去看下,其实主线程单元测试结束容器bean实例已经释放了,所以会报链接不存在。

image.png

1.5 如何解决

解决的方案也很简单在线程池配置的时候配置参数

executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
复制代码

在执行测试案例已经和预期结果一致了。

image.png

线程池安全关闭原理分析

2.1 终止线程的4种方式

  • 正常运行结束
  • 使用退出标志退出线程
  • Interrupt 方法结束线程、
  • stop 方法终止线程(线程不安全),程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,

2.2 线程池提供的shutdown和shutdownnow方法

2.2.1 shutdown 主要流程

调用线程池的shuwdown方法时,

  1. 如果线程正在执行线程池里的任务,即便任务处于阻塞状态,线程也不会被中断,而是继续执行。
  2. 如果线程池阻塞等待从队列里读取任务,则会被唤醒,但是会继续判断队列是否为空,如果不为空会继续从队列里读取任务,为空则线程退出。

image.png

image.png

2.2.2 shutdownnow 主要流程

调用线程池的shutdownNow

1)如果线程正在getTask方法中执行,则会通过for循环进入到if语句,于是getTask返回null,从而线程退出。不管线程池里是否有未完成的任务。

2) 如果线程因为执行提交到线程池里的任务而处于阻塞状态,则会导致报错(如果任务里没有捕获InterruptedException异常),否则线程会执行完当前任务,然后通过getTask方法返回为null来退出。

image.png

shutdown和shutdown主要区别

shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。

2.3 Spring 线程池关闭的设置

看下Spring线程池关闭的源码shutdown 方法

public void shutdown() {
   if (logger.isDebugEnabled()) {
      logger.debug("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
   }
   if (this.executor != null) {
      // 是否需要等所有的任务都结束
      if (this.waitForTasksToCompleteOnShutdown) {
         this.executor.shutdown();
      }
      else {
         for (Runnable remainingTask : this.executor.shutdownNow()) {
            cancelRemainingTask(remainingTask);
         }
      }
      // 
      awaitTerminationIfNecessary(this.executor);
   }
}
复制代码

awaitTerminationIfNecessary 方法

private void awaitTerminationIfNecessary(ExecutorService executor) {
   if (this.awaitTerminationMillis > 0) {
      try {
         if (!executor.awaitTermination(this.awaitTerminationMillis, TimeUnit.MILLISECONDS)) {
            if (logger.isWarnEnabled()) {
               logger.warn("Timed out while waiting for executor" +
                     (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
            }
         }
      }
      catch (InterruptedException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Interrupted while waiting for executor" +
                  (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
         }
         Thread.currentThread().interrupt();
      }
   }
}
复制代码

最后spring线程池 waitForTasksToCompleteOnShutdown和awaitTerminationMillis需要同时设置,否则会不生效. 当然说了这么多如果系统宕机或者运维同学kill -9 pid的啥安全关闭或者优雅停机都无从谈起。

参考文档

如何优雅实现优雅停机?
ThreadPoolTaskScheduler线程池的优雅关闭

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享