首页 > 其他分享 >Async 注解底层异步线程

Async 注解底层异步线程

时间:2024-06-21 14:20:34浏览次数:26  
标签:异步 实现 Spring 线程 注解 Async

一、前言

开发中我们经常会用到异步方法调用,具体到代码层面,异步方法调用的实现方式有很多种,比如最原始的通过实现 Runnable 接口或者继承 Thread 类创建异步线程,然后启动异步线程;再如,可以直接用 java.util.concurrent 包提供的线程池相关 API 实现异步方法调用。

如果说可以用一行代码快速实现异步方法调用,那是不是比上面方法香很多。

Spring 提供了 Async 注解,就可以帮助我们一行代码搞定异步方法调用。Async 注解用起来是很爽,但是如果不对其底层实现做深入研究,难免有时候也会心生疑虑,甚至会因使用不当,遇见一些让人摸不着头脑的问题。

本文首先将对 Async 注解做简单介绍,然后和大家分享一个我们项目中因 Async 注解使用不当的线上问题,接着再深扒 Spring 源码,对 Async 注解底层异步线程池的实现原理一探究竟。

二、Async 注解简介

Async 注解定义源码

001.jpg

从源码可以看出 @Async 注解定义很简单,只需要关注两点:

  • Target ({ElementType.TYPE, ElementType.METHOD}) 标志 Async 注解可以作用在方法和类上,作用在类上时,类的所有方法可以实现异步调用。

  • String value ( ) default "" 是唯一字段属性,用来指定异步线程池,且该字段有缺省值。

Async 注解异步调用实现原理概述

在 Spring 框架中,Async 注解的实现是通过 AOP 来实现的。具体来说,Async 注解是由 AsyncAnnotationAdvisor 这个切面类来实现的。

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

在 AsyncAnnotationAdvisor 中,会使用 AsyncExecutionInterceptor 来处理 Async 注解。AsyncExecutionInterceptor 是实现了 MethodInterceptor 接口的类,用于拦截被 Async 注解标识的方法的调用,并在一个新的线程中执行这个方法。

通过 AOP 的方式实现 Async 注解的异步执行,Spring 框架可以在方法调用时动态地创建代理对象来实现异步执行,而不需要在业务代码中显式地创建新线程。

总的来说,Async 注解的实现是通过 AOP 机制来实现的,具体的切面类是 AsyncAnnotationAdvisor,它利用 AsyncExecutionInterceptor 来处理被 Async 注解标识的方法的调用,实现方法的异步执行。

三、Async 注解底层异步线程池原理探究

获取 Async 注解线程池主流程解析

进入到 Spring 源码 Async 注解 AOP 切面实现部分,我们重点剖析异步调用实现中线程池是怎么处理的。下图是 org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke 方法的实现,可以看出是调用 determineAsyncExecutor 方法获取异步线程池。 002.jpg AsyncExecutionInterceptor#invoke

下图是 determineAsyncExecutor 方法实现: 003.jpg

004.jpg

上方的图为 AsyncExecutionInterceptor#determineAsyncExecutor,下方的图为 AsyncExecutionAspectSupport#getExecutorQualifier

从代码实现中可以看到 determineAsyncExecutor 获取线程池的大致流程: 005.jpg determineAsyncExecutor 获取线程池流程

如果在使用 Async 注解时指定了自定义线程池比较好理解,如果使用 Async 注解时没有指定自定义线程池,Spring 是怎么处理默认线程池呢?继续深入源码看看 Spring 提供的默认线程池的实现。

Spring 是怎么为 Async 注解提供默认线程池的

Async 注解默认线程池有下面两个方法实现:

  • org.springframework.aop.interceptor.AsyncExecutionInterceptor#getDefaultExecutor

  • org.springframework.aop.interceptor.AsyncExecutionAspectSupport#getDefaultExecutor

006.jpg AsyncExecutionInterceptor#getDefaultExecutor

可以看出 AsyncExecutionInterceptor#getDefaultExecutor 方法比较简单:先尝试调用父类 AsyncExecutionAspectSupport#getDefaultExecutor 方法获取线程池,如果父类方法获取不到线程池再用创建 SimpleAsyncTaskExecutor 对象作为 Async 的线程池返回。

007.jpg AsyncExecutionAspectSupport#getDefaultExecutor

再来看父类 AsyncExecutionAspectSupport#getDefaultExecutor 方法的实现,可以看到 Spring 根据类型从 Spring 容器中获取 TaskExecutor 类的实例,先记住这个关键点

我们知道,Spring 根据类型获取实例时,如果 spring 容器中有且只有一个指定类型的实例对象,会直接返回,否则的话,会抛出 NoUniqueBeanDefinitionException 异常或者 NoSuchBeanDefinitionException 异常。

但是,对于 Executor 类型,Spring 容器却 "网开一面",有一个特殊处理:当从 Spring 容器中获取 Executor 实例对象时,如果满足 @ConditionalOnMissingBean (Executor.class) 条件,Spring 容器会自动装载一个 ThreadPoolTaskExecutor 实例对象,而 ThreadPoolTaskExecutor 是 TaskExecutor 的实现类

008.jpg

009.jpg

上方的图为 TaskExecutionAutoConfiguration,下方的图为 TaskExecutionProperties

从 TaskExecutionProperties 和 TaskExecutionAutoConfiguration 两个配置类我们看到 Spring 自动装载的 ThreadPoolTaskExecutor 线程池对象的参数:核心线程数 = 8;最大线程数 = Integer.MAX_VALUE;队列大小 = Integer.MAX_VALUE。

四、总结

现在 Async 注解线程池源码已经看的差不多了,下面这张图是 Spring 处理 Async 异步线程池的流程: 100.jpg Async 异步线程池获取流程

归纳一下:如果在使用 Async 注解时没有指定自定义的线程池会出现以下几种情况:

  • 当 Spring 容器中有且仅有一个 TaskExecutor 实例时,Spring 会用这个线程池来处理 Async 注解的异步任务,这可能会踩坑,如果这个 TaskExecutor 实例是第三方 jar 引入的,可能会出现很诡异的问题。

  • Spring 创建一个核心线程数 = 8、最大线程数 = Integer.MAX_VALUE、队列大小 = Integer.MAX_VALUE 的线程池 来处理 Async 注解的异步任务,这时候也可能会踩坑,由于线程池参数设置不合理,核心线程数 = 8,队列大小过大,如果有大批量并发任务,可能会出现 OOM

  • Spring 创建 SimpleAsyncTaskExecutor 实例 来处理 Async 注解的异步任务,SimpleAsyncTaskExecutor 不是一个好的线程池实现类,SimpleAsyncTaskExecutor 根据需要在当前线程或者新线程中执行异步任务。如果当前线程已经有空闲线程可用,任务将在当前线程中执行,否则将创建一个新线程来执行任务。由于这个线程池没有线程管理的能力,每次提交任务都实时创建新城,所以如果任务量大,会导致性能下降

标签:异步,实现,Spring,线程,注解,Async
From: https://www.cnblogs.com/luobozhijia/p/18260440

相关文章

  • 进程间和线程间通信方式
    在计算机科学领域,进程(process)和线程(thread)是两个基本的执行单元。进程是独立的程序运行实例,拥有自己的内存空间和资源;线程则是进程内部的执行路径,可以共享进程的内存和资源。为了实现进程或线程之间的数据交换和协调,需要采用不同的通信方式。以下我们详细讨论几种常见的进程间......
  • java synchronized 保护线程安全
    前言工作中自己实现了一个MySessionContext类,在实现addSession方法的时候,考虑到会有线程不安全问题,这里需要使用synchronized关键字来保护线程安全。理解synchronized关键字需要了解多线程和线程安全的基本概念。在多线程环境中,多个线程可以同时访问共享资源(例如内存中的变量......
  • 【python】 多线程
    什么是多线程?在操作系统上,所有的应用程序都是通过进程来运行的。当一个应用程序启动时,操作系统会为该应用程序创建一个或多个进程,并为这些进程分配必要的资源,如内存空间、文件句柄等,以协助应用程序的运行。在进程内部,可以同时存在多个线程。这些线程共享同一进程的地址空间和其......
  • JavaScript async await 使用
    你习惯在js代码中使用asyncawait吗?我经常在js代码中写一些异步方法,使用await调用的地方,如果方便修改成异步方法,就修改成异步方法,如下所示:asyncsetPosition(graphic,lng,lat){this.lng=lng;this.lat=lat;if(graphic){letheight=awaitgetHeightByLng......
  • STM32同步通信与异步通信的区别及特点
    1.同步通信同步通信是指通信双方在通信过程中需要使用同步信号进行同步,以确保数据的正确传输。STM32的同步通信主要有两种方式:SPI和I2C。-SPI(SerialPeripheralInterface):SPI是一种高速的同步串行通信协议,它可以实现STM32与外设之间的高速数据传输。SPI通信需要使用4根线:时钟......
  • CompletableFuture多线程并发处理
    CompletableFuture多线程并发处理   概要  一个接口可能需要调用N个其他服务的接口,这在项目开发中还是挺常见的。举个例子:用户请求获取订单信息,可能需要调用用户信息、商品详情、物流信息、商品推荐等接口,  如果是串行(按顺序依次执行每个任务)执行的话,接口的响应速......
  • 线程池原理
     线程池原理线程池(ThreadPool)是一种多线程处理模式,常用于提高性能和资源利用率,特别是在处理大量短时间任务时。线程池通过预先创建和管理一定数量的线程,来执行任务而不是每次都创建和销毁线程,从而减少线程创建和销毁的开销。 线程池的基本概念-线程池:一个包含多个预先创......
  • C#设计:实现文件的多线程下载
    一、程序设计要求能够在下载过程中显示进度信息(如总大小、已下载大小、进度、下载速度、剩余大小、剩余时间、状态、下载的网址等)。支持从指定的URL下载文件。支持多线程并发下载文件。提供友好的用户界面(UI)来下载。具有良好的可扩展性,能够方便地添加新功能或修改现有功能。代......
  • 线程的6种状态(juc编程)
    1线程状态1.1状态介绍当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?Java中的线程状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:publicclassT......
  • java多线程
    目录多线程的实现方式多线程的第一种实现方式 继承Thread类的方式进行实现多线程的第二种实现方式 实现Runnable接口的方式进行实现利用Callable接口和Future接口方式实现 多线程中常用的成员方法 StringgetName()                返回此线程的名......