spring优雅的使用线程池及优雅的接受异步消息

2,404 阅读5分钟

千篇一律之线程介绍

大部分介绍线程的技术文档都是cv来的,今天我们来点不一样的

  • 线程池 自己创建线程,涉及到线程的创建和销毁,太占用资源,没有必要。 使用jdk的ThreadPoolExecutor, 使用该类来创建线程池,关于该类的具体使用这里不过多涉及
  • sping 中的线程池 spring 中封装了几个定义好的线程池实现(实现了TaskExecutor)
  1. SyncTaskExecutor:在调用者的当前线程同步执行任务
  2. SimpleAsyncTaskExecutor会针对每个任务新建一个线程,运行完线程就停止
  3. ThreadPoolTaskExecutor,最常用的,对ThreadPoolExecutor进行了封装,任务扔进去运行就完了

实际使用

  • 线程池整体工作流程(个人理解,有误指正)
  1. 按照CorePoolSize的大小初始化线程,且这些线程不会被销毁(可以设置参数AllowCoreThreadTimeOut为true把核心线程也销毁)
  2. 当超过核心线程,就把多线程的任务放入QueueCapacity里
  3. 当QueueCapacity满了后,按照MaxPoolSize的设置创建新线程,最多不会超过这个设置。而且这些线程会被在setKeepAliveSeconds时间之后被销毁
  4. 当线程达到MaxPoolSize后就按照RejectedExecutionHandler策略进程拒绝策略,最常用的是CallerRunsPolicy:线程队列满了以后交给调用者执行,一般就是交给主线程来处理。
  • 创建配置类(交给spring管理)

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    
    import java.util.concurrent.Executor;
    import java.util.concurrent.ThreadPoolExecutor;
    
    /**
     * 多线程配置类
     *
     * @author ActStrady
     * @date 2020/7/30
     */
    @Configuration
    @EnableAsync
    public class AsyncConfiguration {
        /**
         * 多线程配置
         *
         * @return 线程
         */
        @Bean("taskExecutor")
        public Executor asyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            // 核心线程数5:线程池创建时候初始化的线程数
            executor.setCorePoolSize(5);
            // 最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
            executor.setMaxPoolSize(10);
            // 缓冲队列500:用来缓冲执行任务的队列
            executor.setQueueCapacity(500);
            // 允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
            executor.setKeepAliveSeconds(60);
            // 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
            executor.setThreadNamePrefix("DailyAsync-");
            // 设置拒绝策略. 替换默认线程池,线程队列满了以后交给调用者执行,也就是同步执行 共四种策略
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            // 等待所有任务结束后再关闭线程池
            executor.setWaitForTasksToCompleteOnShutdown(true);
            executor.initialize();
            return executor;
        }
    }
    

    @EnableAsync注解用来开启线程池配置

  • 关于几个核心配置的解释 CorePoolSize:核心线程数量 MaxPoolSize:最大线程数量,超过这个就会执行拒绝策略 QueueCapacity:任务队列,用来缓存任务 keepAliveSeconds: 除核心线程外的普通线程的超时回收时间 RejectedExecutionHandler:拒绝策略,当线程打满且没有销毁的时候执行的拒绝策略 拒绝策略种类: 1. AbortPolicy,默认的处理方式,简单粗暴,丢弃任务并抛出RejectedExecutionException异常 2. DiscardPolicy:丢弃任务,但是不抛出异常 3. DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程 4. CallerRunsPolicy:由调用线程处理该任务(一般主线程)一般使用该策略

  • 关于线程数量设置 根据java并发编程实战得到下面公式

    CPU 密集型任务:这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1
    I/O 密集型任务:线程数=N(CPU核数)* 2
    混合任务:线程数=N(CPU核数)*(1+WT(线程等待时间)/ST(线程时间运行时间))
    

    翻阅很多资料并不能得出一个最合理的配置或者说完全没有一个确定的方案,到底核心线程数按照这个来设置还是最大线程数按照这个来设置?基本上各自说各自的,先把这块当做一个未知吧 ,以目前的理解来说我个人认为还是最大线程数按照这个设置,这边有一篇美团的把参数动态设置,这几个参数还得按照业务来确定。

  • 注解使用

  1. 无返回值
        @Async("taskExecutor")
        public void test() {
        }
    
    对于无返回值的就很简单,使用上边注入(@Bean("taskExecutor"))的线程配置名称来直接使用(@Async("taskExecutor"))
  2. 有返回值 有返回值的情况我们又可以使用CompletableFuture或者Future来处理,关于这两种方法的区别,我引用廖老师的一段话来说明下

    使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。 从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

    • Future 处理
       	@Async("taskExecutor")
       	// 泛型就是你要返回的结果类型
          public Future<String> test() {
          	// 结果就是你返回的结果
          	return new AsyncResult<>("结果");
          }
      
      就像上边廖老师说的调用阻塞方法get(),要么轮询看isDone()是否为true,这样的话无法直接表述多个Future 结果之间的依赖性。
    • CompletableFuture处理 liurio.github.io/2019/12/17/…

本文已参与「新人创作礼」活动,一起开启掘金创作之路