Spring之异步任务@Async详解分析
作者:mmseoamin日期:2023-12-14

文章目录

  • 1 异步@Async详解
    • 1.1 引言
    • 1.2 异步说明和原理
    • 1.3 @Async使用
      • 1.3.1 启动类中增加@EnableAsync
      • 1.3.2 方法上加@Async注解
      • 1.4 @Async异步线程池
        • 1.4.1 默认线程池
        • 1.4.3 在配置文件中配置
        • 1.4.3 自定义线程池
          • 1.4.3.1 编写配置类
          • 1.4.3.2 使用自定义线程池
          • 1.4.4 Spring中的线程池(执行器)
          • 1.5 异步中的事务和返回
            • 1.5.1 异步事务
            • 1.5.2 异步返回
            • 1.6 异步不能回调问题

              1 异步@Async详解

              1.1 引言

              在java中异步线程很重要,比如在业务流处理时,需要通知硬件设备,发短信通知用户,或者需要上传一些图片资源到其他服务器这种耗时的操作,在主线程里处理会阻塞整理流程,而且我们也不需要等待处理结果之后再进行下一步操作,这时候就可以使用异步线程进行处理,这样主线程不会因为这些耗时的操作而阻塞,保证主线程的流程可以正常进行。

              最近在项目中使用了很多线程的操作,在这做个记录

              1.2 异步说明和原理

              使用地方说明:

              • 在方法上使用该@Async注解,申明该方法是一个异步任务;
              • 在类上面使用该@Async注解,申明该类中的所有方法都是异步任务;
              • 使用此注解的方法的类对象,必须是spring管理下的bean对象;
              • 要想使用异步任务,需要在主类上开启异步配置,即,配置上@EnableAsync注解

                @Async的原理概括:

                @Async的原理是通过 Spring AOP 动态代理 的方式来实现的。

                Spring 容器启动初始化bean时,判断类中是否使用了@Async注解,如果使用了则为其创建切入点和切入点处理器,根据切入点创建代理,在线程调用@Async注解标注的方法时,会调用代理,执行切入点处理器invoke方法,将方法的执行提交给线程池中的另外一个线程来处理,从而实现了异步执行。

                所以,需要注意的一个错误用法是,如果a方法调用它同类中的标注@Async的b方法,是不会异步执行的,因为从a方法进入调用的都是该类对象本身,不会进入代理类。

                因此,相同类中的方法调用带@Async的方法是无法异步的,这种情况仍然是同步。

                1.3 @Async使用

                在Spring中启用@Async:

                • @Async注解在使用时,如果不指定线程池的名称,则使用Spring默认的线程池,Spring默认的线程池为SimpleAsyncTaskExecutor。
                • 方法上一旦标记了这个@Async注解,当其它线程调用这个方法时,就会开启一个新的子线程去异步处理该业务逻辑。

                  1.3.1 启动类中增加@EnableAsync

                  以Spring boot为例,启动类中增加@EnableAsync:

                  @EnableAsync
                  @SpringBootApplication
                  public class ManageApplication {
                      //...
                  }
                  

                  1.3.2 方法上加@Async注解

                  @Component
                  public class MyAsyncTask {
                       @Async
                      public void asyncCpsItemImportTask(Long platformId, String jsonList){
                          //...具体业务逻辑
                      }
                  }
                  

                  1.4 @Async异步线程池

                  1.4.1 默认线程池

                  上面的配置会启用默认的线程池/执行器,异步执行指定的方法。

                  Spring默认的线程池的默认配置:

                  默认核心线程数:8,

                  最大线程数:Integet.MAX_VALUE,

                  队列使用LinkedBlockingQueue,

                  容量是:Integet.MAX_VALUE,

                  空闲线程保留时间:60s,

                  线程池拒绝策略:AbortPolicy

                  缺点:从最大线程数的配置上,相信看到问题:并发情况下,会无限创建线程

                  默认线程池的上述缺陷如何解决:答案是,自定义配置参数就可以了

                  1.4.3 在配置文件中配置

                  spring:
                    task:
                      execution:
                        pool:
                          max-size: 6
                          core-size: 3
                          keep-alive: 3s
                          queue-capacity: 1000
                          thread-name-prefix: name
                  

                  1.4.3 自定义线程池

                  在业务场景中,有时需要使用自己定义的执行器来跑异步的业务逻辑,那该怎么办呢?答案是,自定义线程池。

                  1.4.3.1 编写配置类
                  @Configuration
                  @Data
                  public class ExecutorConfig{
                      //核心线程
                      private int corePoolSize;
                      //最大线程
                      private int maxPoolSize;
                      //队列容量
                      private int queueCapacity;
                      //保持时间
                      private int keepAliveSeconds;
                      //名称前缀
                      private String preFix;
                   
                      @Bean("MyExecutor")
                      public Executor myExecutor() {
                          ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
                          executor.setCorePoolSize(corePoolSize);
                          executor.setMaxPoolSize(maxPoolSize);
                          executor.setQueueCapacity(queueCapacity);
                          executor.setKeepAliveSeconds(keepAliveSeconds);
                          executor.setThreadNamePrefix(preFix);
                          executor.setRejectedExecutionHandler( new ThreadPoolExecutor.AbortPolicy());
                          executor.initialize();
                          return executor;
                      }
                  }
                  
                  1.4.3.2 使用自定义线程池

                  在方法上的@Async注解处指定线程池名字:

                  @Component
                  public class MyAsyncTask {
                       @Async("MyExecutor") //使用自定义的线程池(执行器)
                      public void asyncCpsItemImportTask(Long platformId, String jsonList){
                          //...具体业务逻辑
                      }
                  }
                  

                  1.4.4 Spring中的线程池(执行器)

                  Spring用TaskExecutor和TaskScheduler接口提供了异步执行和调度任务的抽象。

                  Spring的TaskExecutor和java.util.concurrent.Executor接口时一样的,这个接口只有一个方法execute(Runnable task)。

                  Spring已经内置了许多TaskExecutor的实现,没有必要自己去实现:

                  • SimpleAsyncTaskExecutor: 这种实现不会重用任何线程,每次调用都会创建一个新的线程。
                  • SyncTaskExecutor: 这种实现不会异步的执行,相反,每次调用都在发起调用的线程中执行。它的主要用处是在不需要多线程的时候,比如简单的测试用例;
                  • ConcurrentTaskExecutor:这个实现是对Java 5 java.util.concurrent.Executor类的包装。有另一个ThreadPoolTaskExecutor类更为好用,它暴露了Executor的配置参数作为bean属性。

                    点击了解Spring线程池ThreadPoolTaskExecutor讲解

                  • SimpleThreadPoolTaskExecutor: 这个实现实际上是Quartz的SimpleThreadPool类的子类,它会监听Spring的生命周期回调。当有线程池,需要在Quartz和非Quartz组件中共用时,这是它的典型用处。
                  • ThreadPoolTaskExecutor:这是最常用、最通用的一种实现。它包含了java.util.concurrent.ThreadPoolExecutor的属性,并且用TaskExecutor进行包装。

                    1.5 异步中的事务和返回

                    1.5.1 异步事务

                    在@Async标注的方法,同时也使用@Transactional进行标注;在其调用数据库操作之时,将无法产生事务管理的控制,原因就在于其是基于异步处理的操作。

                    那该如何给这些操作添加事务管理呢?

                    可以将需要事务管理操作的方法放置到异步方法内部,在内部被调用的方法上添加@Transactional

                    示例:

                    • 方法A:使用了@Async/@Transactional来标注,但是无法产生事务控制的目的。
                    • 方法B:使用了@Async来标注,B中调用了C、D,C/D分别使用@Transactional做了标注,则可实现事务控制的目的

                      1.5.2 异步返回

                      异步的业务逻辑处理场景 有两种:一个是不需要返回结果,另一种是需要接收返回结果。不需要返回结果的比较简单,就不多说了。

                      需要接收返回结果的示例如下:

                      @Async("MyExecutor")
                      public Future> queryMap(List ids) {
                          List<> result = businessService.queryMap(ids);
                          ..............
                          Map resultMap = Maps.newHashMap();
                          ...
                          return new AsyncResult<>(resultMap);
                      }
                      

                      调用异步方法的示例:

                      public Map asyncProcess(List bindDevices,List bindStaffs, String dccId) {
                            Map finalMap =null;
                            // 返回值:
                            Future> asyncResult = MyService.queryMap(ids);
                            try {
                                finalMap = asyncResult.get();
                            } catch (Exception e) {
                                ...
                            }
                            return finalMap;
                      }
                      

                      1.6 异步不能回调问题

                      使用了异步但是执行异步的方法,原因是在方法上加了@Async注解,之所以加这个注解是因为报错:

                      There was an unexpected error (type=Internal Server Error, status=500).

                      Async support must be enabled on a servlet and for all filters involved in async request processing. This is done in Java code using the Servlet API or by adding true to servlet and filter declarations in web.xml

                      异步测试时一直报这个错误,提示我在web.xml开启异步支持,但是我是SpringBoot项目,于是开始网上查找

                      错误:加@Async注解,会更加异步,不能获取异步结果

                      正确:根本原因是容器注册问题,在springboot启动类的注解@SpringBootApplication旁边添加了@ServletComponentScan,才导致上面的报错和不能回调,有三种解决方法:

                      • 去掉注解@ServletComponentScan
                      • 添加容器注册(springboot项目)
                          @Bean
                            public ServletRegistrationBean dispatcherServlet() {
                                ServletRegistrationBean registration = new ServletRegistrationBean(
                                        new DispatcherServlet(), "/");
                                registration.setAsyncSupported(true);
                                return registration;
                            }
                            @Bean
                            DispatcherServlet dispatcherServlet(){
                                return new DispatcherServlet();
                            }
                        

                        在过滤器那里添加asyncSupported = true的支持

                        @WebFilter(urlPatterns="/*",asyncSupported = true)
                        
                        • 修改web.xml(传统xml项目)

                          需要在 web.xml 文件中的 servlet 定义中添加:"true"

                          
                          
                          	Archetype Created Web Application
                          	
                          		contextConfigLocation
                          		classpath:spring-mybatis.xml
                          	
                          	
                          		spring.profiles.active
                          		dev
                          	
                          	
                          		spring.profiles.default
                          		dev
                          	
                          	
                          		encodingFilter
                          		org.springframework.web.filter.CharacterEncodingFilter
                          		true
                          		
                          			encoding
                          			UTF-8
                          		
                          		
                          			forceEncoding
                          			true
                          		
                          	
                          	
                          		encodingFilter
                          		/*
                          	
                          	
                          		org.springframework.web.context.ContextLoaderListener
                          	
                          	
                          		SpringMVC
                          		org.springframework.web.servlet.DispatcherServlet
                          		
                          			contextConfigLocation
                          			classpath:spring-mvc.xml
                          		
                          		1
                          		true
                          	
                          	
                          		SpringMVC
                          		/