Spring中异步注解@Async的使用、原理及使用时可能导致的问题

您所在的位置:网站首页 @async循环依赖 Spring中异步注解@Async的使用、原理及使用时可能导致的问题

Spring中异步注解@Async的使用、原理及使用时可能导致的问题

2024-07-13 14:25| 来源: 网络整理| 查看: 265

前言

很多同学碰到了下面这个问题,添加了Spring提供的一个异步注解@Async循环依赖无法被解决了,下面是一些读者的留言跟群里同学碰到的问题:

本着讲一个知识点就要讲明白、讲透彻的原则,我决定单独写一篇这样的文章对@Async这个注解做一下详细的介绍,这个注解带来的问题远远不止循环依赖这么简单,如果对它不够熟悉的话建议慎用。

文章要点@Async的基本使用

这个注解的作用在于可以让被标注的方法异步执行,但是有两个前提条件 1. 配置类上添加@EnableAsync注解 2. 需要异步执行的方法的所在类由Spring管理 3. 需要异步执行的方法上添加了@Async注解 ”

我们通过一个Demo体会下这个注解的作用吧

第一步,配置类上开启异步:

代码语言:javascript复制@EnableAsync @Configuration @ComponentScan("com.dmz.spring.async") public class Config { }

第二步,

代码语言:javascript复制@Component // 这个类本身要被Spring管理 public class DmzAsyncService { @Async // 添加注解表示这个方法要异步执行 public void testAsync(){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("testAsync invoked"); } }

第三步,测试异步执行

代码语言:javascript复制public class Main { public static void main(String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); DmzAsyncService bean = ac.getBean(DmzAsyncService.class); bean.testAsync(); System.out.println("main函数执行完成"); } } // 程序执行结果如下: // main函数执行完成 // testAsync invoked

通过上面的例子我们可以发现,DmzAsyncService中的testAsync方法是异步执行的,那么这背后的原理是什么呢?我们接着分析

原理分析

我们在分析某一个技术的时候,最重要的事情是,一定一定要找到代码的入口,像Spring这种都很明显,入口必定是在@EnableAsync这个注解上面,我们来看看这个注解干了啥事(本文基于5.2.x版本)

代码语言:javascript复制@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented // 这里是重点,导入了一个ImportSelector @Import(AsyncConfigurationSelector.class) public @interface EnableAsync { // 这个配置可以让程序员配置需要被检查的注解,默认情况下检查的就是@Async注解 Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); // 异步执行嘛,先获取到一个线程池 AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod); if (executor == null) { throw new IllegalStateException( "No executor specified and no default executor set on AsyncExecutionInterceptor either"); } // 然后将这个方法封装成一个 Callable对象传入到线程池中执行 Callable task = () -> { try { Object result = invocation.proceed(); if (result instanceof Future) { return ((Future) result).get(); } } catch (ExecutionException ex) { handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); } catch (Throwable ex) { handleError(ex, userDeclaredMethod, invocation.getArguments()); } return null; }; // 将任务提交到线程池 return doSubmit(task, executor, invocation.getMethod().getReturnType()); } 导致的问题及解决方案问题1:循环依赖报错

就像在这张图里这个读者问的问题,

分为两点回答:

第一:循环依赖为什么不能被解决?

这个问题其实很简单,在《面试必杀技,讲一讲Spring中的循环依赖》这篇文章中我从两个方面分析了循环依赖的处理流程

简单对象间的循环依赖处理AOP对象间的循环依赖处理

按照这种思路,@Async注解导致的循环依赖应该属于AOP对象间的循环依赖,也应该能被处理。但是,重点来了,解决AOP对象间循环依赖的核心方法是三级缓存,如下:

在三级缓存缓存了一个工厂对象,这个工厂对象会调用getEarlyBeanReference方法来获取一个早期的代理对象的引用,其源码如下:

代码语言:javascript复制protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { // 看到这个判断了吗,通过@EnableAsync导入的后置处理器 // AsyncAnnotationBeanPostProcessor根本就不是一个SmartInstantiationAwareBeanPostProcessor // 这就意味着即使我们通过AsyncAnnotationBeanPostProcessor创建了一个代理对象 // 但是早期暴露出去的用于给别的Bean进行注入的那个对象还是原始对象 if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }

看完上面的代码循环依赖的问题就很明显了,因为早期暴露的对象跟最终放入容器中的对象不是同一个,所以报错了。报错的具体位置我在你知道Spring是怎么将AOP应用到Bean的生命周期中的吗? 文章末尾已经分析过了,本文不再赘述

解决方案

就以上面读者给出的Demo为例,只需要在为B注入A时添加一个@Lazy注解即可

代码语言:javascript复制@Component public class B implements BService { @Autowired @Lazy private A a; public void doSomething() { } }

这个注解的作用在于,当为B注入A时,会为A生成一个代理对象注入到B中,当真正调用代理对象的方法时,底层会调用getBean(a)去创建A对象,然后调用方法,这个注解的处理时机是在org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency方法中,处理这个注解的代码位于org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver#buildLazyResolutionProxy,这些代码其实都在我之前的文章中分析过了

《Spring杂谈 | Spring中的AutowireCandidateResolver》

《谈谈Spring中的对象跟Bean,你知道Spring怎么创建对象的吗?》

所以本文不再做详细分析

问题2:默认线程池不会复用线程

我觉得这是这个注解最坑的地方,没有之一!我们来看看它默认使用的线程池是哪个,在前文的源码分析中,我们可以看到决定要使用线程池的方法是org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor。其源码如下:

代码语言:javascript复制protected AsyncTaskExecutor determineAsyncExecutor(Method method) { AsyncTaskExecutor executor = this.executors.get(method); if (executor == null) { Executor targetExecutor; // 可以在@Async注解中配置线程池的名字 String qualifier = getExecutorQualifier(method); if (StringUtils.hasLength(qualifier)) { targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier); } else { // 获取默认的线程池 targetExecutor = this.defaultExecutor.get(); } if (targetExecutor == null) { return null; } executor = (targetExecutor instanceof AsyncListenableTaskExecutor ? (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor)); this.executors.put(method, executor); } return executor; }

最终会调用到org.springframework.aop.interceptor.AsyncExecutionInterceptor#getDefaultExecutor这个方法中

代码语言:javascript复制protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { Executor defaultExecutor = super.getDefaultExecutor(beanFactory); return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor()); }

可以看到,它默认使用的线程池是SimpleAsyncTaskExecutor。我们不看这个类的源码,只看它上面的文档注释,如下:

主要说了三点

为每个任务新起一个线程默认线程数不做限制不复用线程

就这三点,你还敢用吗?只要你的任务耗时长一点,说不定服务器就给你来个OOM。

解决方案

最好的办法就是使用自定义的线程池,主要有这么几种配置方法

在之前的源码分析中,我们可以知道,可以通过AsyncConfigurer来配置使用的线程池

如下:

代码语言:javascript复制public class DmzAsyncConfigurer implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { // 创建自定义的线程池 } } 直接在@Async注解中配置要使用的线程池的名称

如下:

代码语言:javascript复制public class A implements AService { private B b; @Autowired public void setB(B b) { System.out.println(b); this.b = b; } @Async("dmzExecutor") public void doSomething() { } } 代码语言:javascript复制@EnableAsync @Configuration @ComponentScan("com.dmz.spring.async") @Aspect public class Config { @Bean("dmzExecutor") public Executor executor(){ // 创建自定义的线程池 return executor; } } 总结

本文主要介绍了Spring中异步注解的使用、原理及可能碰到的问题,针对每个问题文中也给出了方案。希望通过这篇文章能帮助你彻底掌握@Async注解的使用,知其然并知其所以然!



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3