Async的原理与@Lazy的说明

1. 文章要点

在这里插入图片描述

2. @Async的基本使用

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

我们通过一个Demo体会下这个注解的作用吧
第一步,配置类上开启异步:

@EnableAsync
@Configuration
@ComponentScan("com.dmz.spring.async")
public class Config {
    
    

}

第二步,

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

第三步,测试异步执行

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 方法是异步执行的,那么这背后的原理是什么呢?我们接着分析

3. 原理分析

详情见原文
@Async注解的就是通过AsyncAnnotationBeanPostProcessor这个后置处理器生成一个代理对象来实现异步的,接下来我们就具体看看 AsyncAnnotationBeanPostProcessor 是如何生成代理对象的,我们主要关注一下几点即可:

  1. 是在生命周期的哪一步完成的代理?
    postProcessAfterInitialization 方法中
  2. 切点的逻辑是怎么样的?它会对什么样的类进行拦截?
  3. 通知的逻辑是怎么样的?是如何实现异步的?

4. 导致的问题及解决方案

4.1 问题1:循环依赖报错

就像在这张图里这个读者问的问题,
在这里插入图片描述

分为两点回答:
第一:循环依赖为什么不能被解决?
这个问题其实很简单,在 《面试必杀技,讲一讲Spring中的循环依赖》这篇文章中我从两个方面分析了循环依赖的处理流程

  1. 简单对象间的循环依赖处理
  2. AOP对象间的循环依赖处理
    按照这种思路,@Async 注解导致的循环依赖应该属于AOP对象间的循环依赖,也应该能被处理。但是,重点来了,解决AOP对象间循环依赖的核心方法是三级缓存,如下:
    在这里插入图片描述

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

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;
}

看完上面的代码循环依赖的问题就很明显了,因为早期暴露的对象跟最终放入容器中的对象不是同一个,所以报错了。
在这里插入图片描述

4.2 问题一解决方案

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

@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怎么创建对象的吗?》

所以本文不再做详细分析

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

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

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这个方法中

protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
    
    
   Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
   return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
}

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

主要说了三点

  1. 为每个任务新起一个线程
  2. 默认线程数不做限制
  3. 不复用线程
    就这三点,你还敢用吗?只要你的任务耗时长一点,说不定服务器就给你来个OOM。

4.4 解决方案

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

  1. 在之前的源码分析中,我们可以知道,可以通过 AsyncConfigurer 来配置使用的线程池
    如下:
public class DmzAsyncConfigurer implements AsyncConfigurer {
    
    
   @Override
   public Executor getAsyncExecutor() {
    
    
      // 创建自定义的线程池
   }
}

  1. 直接在@Async注解中配置要使用的线程池的名称
    如下:
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() {
    
    
    }
}

@EnableAsync
@Configuration
@ComponentScan("com.dmz.spring.async")
@Aspect
public class Config {
    
    
    @Bean("dmzExecutor")
    public Executor executor(){
    
    
        // 创建自定义的线程池
        return executor;
    }
}

5. @Lazy的说明

引用自:https://blog.csdn.net/m0_43448868/article/details/112005140

  • 当Bean的依赖注入完成后,其属性注入的实际上是一个使用JDK动态代理或者CGLIB创建出来的代理对象,只有用户去调用目标对象的方法时,才会触发去真正完成依赖解析

  • 当将@Lazy注解添加到字段或者方法上的参数上,IoC容器将会为其创建代理对象,如果在Bean上面添加@Lazy注解,并且在每个依赖该Bean的地方都添加上@Lazy注解,那么该Bean不会在IoC容器初始化的时候就进行实例化,只有到调用该Bean的方法时,IoC容器才会真正的去实例化Bean

  • 如果仅在Bean上面添加@Lazy注解,其它依赖该Bean的地方不添加@Lazy注解并且每个依赖该Bean的Bean并非全部都被@Lazy注解标记,那么即使添加了@Lazy注解,也依然会在IoC容器初始化的时候进行实例化

  • 说起来有点绕口,如果有小伙伴未能理解,可以根据以上代码多测试几次就会明白,实在不明白,欢迎在评论区给我留言哦

6. 总结

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

猜你喜欢

转载自blog.csdn.net/weixin_44063529/article/details/143218303