阿里面试:说说@Async实现原理?

@Async 是 Spring 3.0 提供的一个注解,用于标识某类(下的公共方法)或某方法会执行异步调用。

接下来,我们来看下 @Async 的基本使用和实现原理。

1.基本使用

@Async 基本使用可以分为以下 3 步:

  1. 项目中开启异步支持
  2. 创建异步方法
  3. 调用异步方法

1.1 开启异步支持

以 Spring Boot 项目为例,我们首先需要在 Spring Boot 的启动类,也就是带有@SpringBootApplication 注解的类上添加 @EnableAsync 注解,以开启异步方法执行的支持,如下代码所示:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

1.2 创建异步方法

创建异步方法是在需要异步执行的方法上添加 @Async 注解,这个方法一定是要放在被 IoC 容器管理的 Bean 中,只有被 IoC 管理的类才能实现异步调用,例如在带有 @Service 注解的类中创建异步方法:

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncService {

    @Async
    public void performAsyncTask() {
        // 这里放置需要异步执行的代码
        System.out.println("异步任务正在执行,当前线程:" + Thread.currentThread().getName());
    }
}

1.3 调用异步方法

在其他类或方法中,通过注入这个服务类的实例来调用异步方法。注意,直接在同一个类内部调用不会触发异步行为,必须通过注入的实例调用,使用 new 创建的对象也不能进行异步方法调用,具体实现代码如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

    @Autowired
    private AsyncService asyncService;

    @GetMapping("/startAsync")
    public String startAsyncTask() {
        asyncService.performAsyncTask();
        return "异步任务已启动";
    }
}

2.实现原理

简单来说,@Async 注解是由 AOP(面向切面)实现的,具体来说,它是由 AsyncAnnotationAdvisor 这个切面类来实现的。

在 AsyncAnnotationAdvisor 中,会使用 AsyncExecutionInterceptor 来处理 @Async 注解,它会在被 @Async 注解标识的方法被调用时,创建一个异步代理对象来执行方法。这个异步代理对象会在一个新的线程中调用被 @Async 注解标识的方法,从而实现方法的异步执行。

在 AsyncExecutionInterceptor 中,核心方法是 getDefaultExecutor 方法,使用此方法来获取一个线程池来执行被 @Async 注解修饰的方法,它的实现源码如下:

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

此方法实现比较简单,它是先尝试调用父类 AsyncExecutionAspectSupport#getDefaultExecutor 方法获取线程池,如果父类方法获取不到线程池再用创建 SimpleAsyncTaskExecutor 对象作为 Async 的线程池返回。

而 SimpleAsyncTaskExecutor 中在执行任务时是这样的:

protected void doExecute(Runnable task) {
    this.newThread(task).start();
}

可以看出,在 Spring 框架中如果使用默认的 @Async 注解,它的执行比较简单粗暴,并没有使用线程池,而是每次创建线程来执行,所以在 Spring 框架中是不能直接使用 @Async 注解的,需要使用 @Async 注解搭配自定义的线程池,既实现 AsyncConfigurer 接口来提供自定义的 ThreadPoolTaskExecutor 来创建线程池,以确保 @Async 能真正的使用线程池来执行异步任务。

然而,在 Spring Boot 中,因为在框架启动时,自动注入了 ThreadPoolTaskExecutor,如下源码所示:

@ConditionalOnClass({ThreadPoolTaskExecutor.class})
@AutoConfiguration
@EnableConfigurationProperties({TaskExecutionProperties.class})
@Import({TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.TaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.TaskExecutorConfiguration.class})
public class TaskExecutionAutoConfiguration {
    public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor";

    public TaskExecutionAutoConfiguration() {
    }
}

具体的构建细节源码如下:

@Bean
@ConditionalOnMissingBean({TaskExecutorBuilder.class, ThreadPoolTaskExecutorBuilder.class})
ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder(TaskExecutionProperties properties, ObjectProvider<ThreadPoolTaskExecutorCustomizer> threadPoolTaskExecutorCustomizers, ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers, ObjectProvider<TaskDecorator> taskDecorator) {
    TaskExecutionProperties.Pool pool = properties.getPool();
    ThreadPoolTaskExecutorBuilder builder = new ThreadPoolTaskExecutorBuilder();
    builder = builder.queueCapacity(pool.getQueueCapacity());
    builder = builder.corePoolSize(pool.getCoreSize());
    builder = builder.maxPoolSize(pool.getMaxSize());
    builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
    builder = builder.keepAlive(pool.getKeepAlive());
    TaskExecutionProperties.Shutdown shutdown = properties.getShutdown();
    builder = builder.awaitTermination(shutdown.isAwaitTermination());
    builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
    builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
    Stream var10001 = threadPoolTaskExecutorCustomizers.orderedStream();
    Objects.requireNonNull(var10001);
    builder = builder.customizers(var10001::iterator);
    builder = builder.taskDecorator((TaskDecorator)taskDecorator.getIfUnique());
    builder = builder.additionalCustomizers(taskExecutorCustomizers.orderedStream().map(this::adapt).toList());
    return builder;
}

因此在 Spring Boot 框架中可以直接使用 @Async 注解,无需担心它每次都会创建线程来执行的问题

课后思考

为什么使用 @Async 注解不能解决循环依赖的问题?为什么使用 @Async 注解会导致事务实现?

#java#
全部评论
状态机啊
点赞 回复 分享
发布于 07-05 20:48 上海

相关推荐

#我的失利项目复盘##java##java面试题##java项目##项目#在给别人做模拟面试时,看到的项目某某商城某某商城为某某APP&nbsp;首页核心业务之一,用户通过首页可直接接入到商城进行浏览、购物行为。商城团队历经了历次大促、活动促销的考验,我司作为合作方与某某大厂架构团队一起完成商城平台的开发建设;我作为核心开发人员参与了商城商品中台建设、库存中心、微服务改造升级、子系统接入、分库分表、拼团营销、金币抽签、合并支付等业务与架构工作。·技术难点:库存中心·技术挑战:商城经常做促销活动、秒杀场景,&nbsp;商品瞬时进行库存扣减造成&nbsp;redis&nbsp;机器流量倾斜问题。&nbsp;【秒杀场景需不需要单独的服务器,秒杀场景是否需要加锁;促销活动中,活动资源如何评定;】·技术方案:实现了一个高性能,可支撑分桶多分片的库存中心,提供单库存分片不足扣减的合并库存功能,提供商品操作库存入库的渐进性入缓存的实现。【如何保证数据的一致性;分桶多分片的库存扣减完整业务/时序图】·技术难点:数据迁移平台·技术挑战:分库分表方案敲定,为了实现数据迁移、数据同步需要完善可靠的数据迁移系统【数据量有多大?分库分表后,如何保证数据闭环,业务是否需要修改】·技术方案:研发数据迁移系统,包括了业务表(订单、订单详情等),迁移表、迁移配置表等。实现了全量同步滚动拉取、增量同步(基于&nbsp;Cancal+MQ)防止数据丢失和高效写入方案。实现了单库到八库八表的实践。&nbsp;【binlog和redolog的区别,binlog异步迁移是否存在风险,是否会造成查询过程卡顿】社交分享平台&nbsp;【刚培训结束,自己做的面试项目】项目介绍&nbsp;:社交分享电商平台旨在为用户提供分享购物心得,并可以直接购买相关商品。该项目分为管理端和用户端。用户端核心业务有:查看附近门店、发布笔记(获取积分)、签到、点赞和收藏,以此带动用户的活跃度。还可以发放一些优惠券,促进用户消费。管理端核心业务有:用户的笔记、评论、商品、优惠券的管理。 使用技术&nbsp;SpringBoot、SpringCloud、RabbitMQ、Redis、XXL-JOB、Redisson、Mybatis-plus、分库分表等。&nbsp;【接口性能主要在哪些地方消耗较大:IO】工作职责 1、采用百度地址编码和路线规划服务,为用户和商家地址提供查找附近门店的功能。 2、采用Redis位图&nbsp;,优化用户签到功能,减少内存消耗。&nbsp;【按照每周讲解一下位图怎么使用:0000000---》0000001----&gt;0000011】3、采用Redis&nbsp;SortedSet数据结构存储用户本月积分排行,使用MySQL分表存储历史积分排行。&nbsp;【积分排行持久化的作用】4、采用XXL-JOB实现分布式任务调度,定时持久化上赛季积分排行,根据当月和历史积分排行发送优惠券。&nbsp;【历史数据较多时,如何进行数据对比?】5、开发可扩展和通用的点赞/踩模块,利用Redis&nbsp;Set数据结构存储用户点赞明细,&nbsp;SortedSet数据结构存储特定业务项的点赞总数,增加用户互动性。&nbsp;【为什么要用有序集合存储点赞明细】6、采用RabbitMQ实现消息队列,将点赞总数放入消息队列,实现系统解耦和异步处理,&nbsp;提升系统响应速度。【点赞总数在什么场景下会被持久化DB层;场景如何界定】&nbsp;7、设计支持多类型配置的优惠券系统,采用策略模式选择不同类型的优惠券。 8、解决了超发和超领问题。使用JMeter工具进行压测,确保并发安全和提高用户体验和平台性能。 9、采用乐观锁思想解决优惠券超发问题,利用关系型数据库写锁排他性保证并发安全。&nbsp;【如何不使用锁解决超发问题?】10、采用Redisson分布式锁解决优惠券超领问题,结合SPEL表达式、工厂模式、策略模式和AOP思想,封装自定义分布式锁注解。个人成就 1、查询点赞状态使用&nbsp;,从单命令执行,改为RedisTemplate&nbsp;的&nbsp;executePipelined&nbsp;方法进行批量处理,优化系统响应时间(&nbsp;40s-&gt;100ms)。&nbsp;2、多线程优化大批量数据插入速度:由于cdk的生成需要将这批码插入数据库中保存,当创建十万条cdk记录时,耗时达到了十几秒。使用@Async+自定义线程池的方式,异步生成cdk,优化后执行耗时2.5秒,执行时间缩短了6倍。&nbsp;【批量插入的数据是否会受到间隙锁的影响;数据在插入过程中,如何避免/减少页分裂/页合并的产生/受到间隙锁的影响】3&nbsp;、基于Redis异步领券响应速度:在兌换资格校验的时候,或者领券资格校验的时候,会有多次与Redis的交互,每一次交互都需要发起一次网络请求。在并发较高的情况下导致网络拥堵,导致业务变慢。通过编写LUA脚本,在脚本中编写复杂业务判断,通过一次请求,完成对脚本的调用。提高接口响应速度。
查看11道真题和解析 我的失利项目复盘 Java求职圈
点赞 评论 收藏
分享
点赞 5 评论
分享
牛客网
牛客企业服务