提升Spring Boot应用性能的秘密武器:揭秘@Async注解的实用技巧
  J6X4jGS7eKRb 2024年03月11日 69 0

引言

在日常业务开发中,异步编程已成为应对并发挑战和提升应用程序性能的关键策略。传统的同步编程方式,由于会阻碍主线程执行后续任务直至程序代码执行结束,不可避免地降低了程序整体效率与响应速度。因此,为克服这一瓶颈,开发者广泛采用异步编程技术,将那些可能阻塞的长时间运行任务委派至后台线程处理,从而确保主线程始终保持高效和灵敏的响应能力。

SpringBoot作为一款广受欢迎的应用开发框架,极大地简化了异步编程实践。其中,@Async注解是SpringBoot为实现异步编程提供的便捷工具之一。通过巧妙地应用@Async注解,开发者能够无缝地将方法调用转化为异步执行模式,进而增强系统的并发性能表现。

本文将深度剖析SpringBoot中的@Async注解,包括其内在原理、具体使用方法以及相关注意事项。我们将深入探讨@Async的工作机制,展示如何在实际的SpringBoot项目中有效运用该注解。

@Async的原理

SpringBoot中,@Async注解的实现原理基于Spring框架的AOP和任务执行器(Task Executor)机制。

@Async的启用

开启对异步方法的支持需要在配置类上添加@EnableAsync注解,然后就可以激活了一个Bean后处理器:AsyncConfigurationSelector,它负责自动配置AsyncConfigurer,为异步方法提供所需的线程池。
image.png

AsyncConfigurationSelector中默认使用PROXY的代理,即使用ProxyAsyncConfiguration,而ProxyAsyncConfiguration是用于配置Spring异步方法的代理模式的配置类。

image.png

当然我们还可以指定使用另外一个代理模式:AdviceMode.ASPECTJ,以便使用AspectJ来进行更高级的拦截和处理。

它继承至AbstractAsyncConfiguration,在AbstractAsyncConfiguration中配置AsyncConfigurersetConfigurers方法用于设置异步任务执行器和异常处理器。

image.png

AsyncConfigurer中提供了一种便捷的方式来配置异步方法的执行器(AsyncTaskExecutor)。通过实现AsyncConfigurer接口,可以自定义异步方法的执行策略、线程池等配置信息。默认情况下Spring会先搜索TaskExecutor类型的bean或者名字为taskExecutorExecutor类型的bean,都不存在使用SimpleAsyncTaskExecutor执行器。

public interface AsyncConfigurer {  
	/*
	* 该方法用于获取一个AsyncTaskExecutor对象,用于执行异步方法。
	* 可以在这个方法中创建并配置自定义的AsyncTaskExecutor,例如ThreadPoolTaskExecutor或SimpleAsyncTaskExecutor等。
	*/
    @Nullable  
    default Executor getAsyncExecutor() {  
       return null;  
    }  

	/*
	* 该方法用于获取一个AsyncUncaughtExceptionHandler对象,用于处理异步方法执行中未捕获的异常。如果异步方法执行过程中出现异常而没有被捕获,Spring会调用这个方法来处理异常。
	* 可以在这个方法中返回自定义的AsyncUncaughtExceptionHandler实现,以实现对异步方法异常的处理逻辑。
	*/
    @Nullable  
    default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {  
       return null;  
    }  
  
}

同时ProxyAsyncConfiguration中的AsyncAnnotationBeanPostProcessor会扫描应用上下文中的所有Bean,检查它们的方法是否标记了@Async注解。对于标记了@Async注解的方法,AsyncAnnotationBeanPostProcessor会创建一个代理对象,用于在调用该方法时启动一个新的线程或使用线程池执行该方法。这样就实现了异步执行的功能。同时它还负责处理@Async注解中的其他属性,例如设置异步方法的执行超时时间、指定线程池名称等。

异步方法注解与代理

当服务类的方法被@Async注解修饰时,Spring AOP会检测到这个注解,并利用动态代理技术为该类创建一个代理对象。其他组件通过Spring容器调用带有@Async注解的方法时,实际上是调用了代理对象的方法。

一个带有@Async注解的方法被调用时,Spring AOP会拦截这个方法调用。此时就会触发处理异步调用的核心拦截器:AsyncExecutionInterceptor。它的主要任务是将被@Async修饰的方法封装成一个Runnable或者Callable任务,并将其提交给TaskExecutor管理的线程池去执行。这个过程确保了异步方法的执行不会阻塞调用者线程。

image.png

TaskExecutor与线程池

TaskExecutor是一个接口,定义了如何执行RunnableCallable任务。SpringBoot提供了多种实现,如SimpleAsyncTaskExecutorThreadPoolTaskExecutor等。通常我们会自定义一个ThreadPoolTaskExecutor以满足特定需求,比如设置核心线程数、最大线程数、队列大小等参数,以确保异步任务能够高效并发执行。AsyncExecutionInterceptor将异步方法封装的任务提交给配置好的TaskExecutor管理的线程池执行。

异步方法执行与结果返回

异步方法的实际执行在独立的线程中进行,不阻塞调用者线程。异步方法的返回类型可以是voi 或者具有返回值,如果异步方法有返回值,那么返回类型通常应该是java.util.concurrent.Future,这样调用者可以通过Future对象来检查异步任务是否完成以及获取最终的结果。

@Async使用

SpringBoot中,使用@Async注解可以轻松地将方法标记为异步执行。下面来看一下如何在Spring Boot项目中正确地使用@Async注解,包括配置方法和注意事项。

在方法上添加@Async注解

要使用@Async注解,首先需要在要异步执行的方法上添加该注解。这样Spring就会在调用这个方法时将其封装为一个异步任务,并交给线程池执行。

@Service  
public class AsyncTaskService {  
  
    /**  
     * 通过@Async 注解表明该方法是个异步方法,  
     * @param i  
     */  
    @Async  
    public void executeAsyncTask(Integer i) {  
        System.out.println(Thread.currentThread().getName()+" 执行异步任务:" + i);  
    }
}    

启用异步功能

SpringBoot应用中,需要在配置类上添加@EnableAsync注解来启用对异步方法的支持。

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

配置线程池

默认情况下,SpringBoot会使用一个默认的线程池来执行异步任务(SimpleAsyncTaskExecutor)。但是,为了更好地控制线程池的行为,我们可以自定义ThreadPoolTaskExecutor,并通过AsyncConfigurer进行配置。

@Configuration  
public class TaskExecutorConfig implements AsyncConfigurer{  
  
  
    @Override  
    public Executor getAsyncExecutor() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        executor.setCorePoolSize(5);  
        executor.setQueueCapacity(25);  
        executor.setMaxPoolSize(10);  
        executor.setThreadNamePrefix("MyAsyncThread-");  
        executor.initialize();  
        return executor;  
    }
 }   

测试

@SpringBootTest  
public class SpringBootBaseApplicationTests {  
  
    @Autowired  
    private AsyncTaskService asyncTaskService;  
  
    @Test  
    public void asyncTest() {  
       for (int i = 0; i < 10; i++) {  
          asyncTaskService.executeAsyncTask(i);  
       }  
    }  
  
}

输出结果如下:

image.png

注意事项

  1. @Async必须配合@EnableAsync注解一起使用。两者缺一不可。

  2. 异步方法必须定义在Spring Bean中,因为Spring AOP是基于代理对象来实现的。假如我们把AsyncTaskService类中的@Service去掉。就不创建Bean。然后测试代码中修改为如下:

@Test  
public void asyncTest() {  
    AsyncTaskService asyncTaskService = new AsyncTaskService();  
    for (int i = 0; i < 10; i++) {  
       asyncTaskService.executeAsyncTask(i);  
    }  
}

执行结果如下:
image.png

都是主线程同步方法。

  1. 异步方法不能定义为private或static,因为Spring AOP无法拦截这些方法。我们修改AsyncTaskService类中的方法修改为private或者static,则会发生编译错误:

image.png
image.png

  1. 异步方法内部的调用不能使用this关键字,因为this关键字是代理对象的引用,会导致异步调用失效。
@Service  
public class AsyncTaskService {
	@Async  
	public void asyncMethod() {  
	    System.out.println("Async method executed in thread: " + Thread.currentThread().getName());  
	}  
	  
	  
	public void callAsyncMethod() {  
	    // 在同一个类中直接调用异步方法  
	    this.asyncMethod(); // 这里调用不会触发异步执行  
	    System.out.println("callAsyncMethod executed in thread: " + Thread.currentThread().getName());  
	}
}	
  1. @Async注解修饰的方法不能直接被同一个类中的其他方法调用。原因是Spring会在运行时生成一个代理类,调用异步方法时实际上是调用这个代理类的方法。因此,如果在同一个类中直接调用异步方法,@Async注解将不会生效,因为这样调用会绕过代理对象,导致异步执行失效。
@Service  
public class AsyncTaskService {

	@Async  
	public void asyncMethod() {  
	    System.out.println("Async method executed in thread: " + Thread.currentThread().getName());  
	}  
	  
	public void callAsyncMethod() {  
	    // 在同一个类中直接调用异步方法  
	    asyncMethod(); // 这里调用不会触发异步执行  
	    System.out.println("callAsyncMethod executed in thread: " + Thread.currentThread().getName());  
	}
}

测试代码:

@SpringBootTest  
public class SpringBootBaseApplicationTests {  
  
    @Autowired  
    private AsyncTaskService asyncTaskService;  
  
    @Test  
    public void asyncTest2(){  
       asyncTaskService.callAsyncMethod();  
    }  
  
}

执行结果如下:

image.png

  1. 不同的异步方法间不要相互调用
    异步方法间的相互调用会显著增加代码的复杂性层级,由于异步执行的本质在于即时返回并延迟完成任务,因此,嵌套或递归式的异步调用容易导致逻辑难以梳理和维护,特别是在涉及多异步操作状态追踪、顺序控制及依赖关系管理时尤为突出。

当异步方法内部进一步调用其他异步方法,并且牵涉到同步资源如锁、信号量等时,极易引发死锁问题。例如,一个线程在等待自身启动的另一个异步任务结果的同时,该任务却尝试获取第一个线程所持有的锁,如此循环等待,形成无法解开的死锁。

无节制地在异步方法内部启动新的异步任务,特别是在高并发场景下,可能导致线程池资源迅速耗尽,使得系统丧失处理更多请求的能力。此外,直接的异步方法调用还增加了错误处理与日志记录的难度,特别是遇到异常情况时,往往难以追溯原始调用链路以精准定位问题源头。

若需要确保异步方法按照特定顺序执行,直接调用会导致逻辑混乱不清。为解决这一问题,通常推荐采用回调机制、Future/CompletionStage链式编程、响应式编程模型(如RxJava、Project Reactor)等方式来确保有序执行并降低耦合度。

同时,频繁且低延迟的任务间直接互相调用可能会引入额外的上下文切换开销,从而对系统的整体性能造成潜在负面影响。

  1. 合理配置线程池
    Spring Boot默认提供的线程池配置可能无法充分满足特定应用在复杂多变生产环境下的需求,例如其预设的线程数、队列大小和拒绝策略等参数可能不尽合理。为确保资源的有效管理和精细控制,我们可以通过自定义线程池来灵活设定核心线程数、最大线程数、线程空闲超时时间、任务等待队列容量以及饱和策略(如任务拒绝策略)等关键属性,从而适应不同业务场景对并发执行任务数量及资源消耗的精准调控。

另外,不同类型异步任务具有不同的执行特性:有的任务耗时较长,而有的则短促且频繁。针对这种情况,为各类任务配置独立的线程池有助于实现更好的资源隔离,避免任务间的相互影响,进而保障系统的稳定性和响应速度。同时,为了满足特定的安全规范或性能要求,自定义线程池还可以支持诸如设置守护线程、优先级、线程命名格式化等功能。

更重要的是,自定义线程池有利于系统内部执行状态的深度监控与问题诊断。通过制定合理的命名规则、详尽的日志记录以及精确的metrics统计分析,我们可以清晰洞察每个线程池的工作状况,及时发现并优化潜在的性能瓶颈。

如果不进行自定义线程池配置,仅依赖于默认或简化的线程池实现,在面对大量涌入的任务时,可能会因线程资源耗尽导致整个系统响应能力和可用性受损。因此,采用合理配置的自定义线程池能够在高负载环境下有效防范此类风险,有力支撑系统的稳健运行。

@Configuration  
public class TaskExecutorConfig implements AsyncConfigurer{  
  
  
    @Override  
    public Executor getAsyncExecutor() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        // 核心线程数  
        executor.setCorePoolSize(5);  
        // 设置队列容量  
        executor.setQueueCapacity(25);  
        // 最大线程数  
        executor.setMaxPoolSize(10);  
        // 自定义线程名称前缀  
        executor.setThreadNamePrefix("MyAsyncThread-");  
        executor.initialize();  
        return executor;  
    }
}    

关于自定义线程池的参数讲解请参考我这篇文章:重温Java基础(二)之Java线程池最全详解

  1. 异常处理:
    异步方法内部的异常通常不会被调用方捕获到,因此需要在异步方法内部进行异常处理,可以通过try-catch块:
@Async  
public void asyncMethod() {  
    try {  
        // 异步操作代码  
    } catch (Exception ex) {  
        log.error("Error occurred in async method", ex);  
        // 其他错误处理逻辑  
    }  
}

或者使用@Async注解的exceptionHandler属性来处理异常并进行适当的日志记录或错误处理。

@Configuration  
public class TaskExecutorConfig implements AsyncConfigurer{

	@Override  
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {  
        return new CustomAsyncExceptionHandler();  
    }  
  
}  

// 自定义异常处理器
class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {  
  
    @Override  
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {  
        log.error("Uncaught exception in async method: " + method.getName(), ex);  
        // 其他错误处理逻辑  
    }  
}
  1. 与事务的交互
    默认情况下,当我们在Spring应用中使用@Async注解标记一个方法为异步执行时,这个方法将不会参与到其调用者所处的事务上下文中。这意味着,如果调用异步方法的方法在一个事务内执行,该事务将在调用异步方法后正常提交或回滚,但异步方法内部的操作并不会受到这个事务的影响。

例如,若在同步方法中修改了数据库记录,并随后调用了一个异步方法来更新其他相关的数据,那么如果同步方法中的事务在调用异步方法后提交,而异步方法在执行过程中抛出了异常导致更新失败,这时第一部分已提交的数据和第二部分未成功更新的数据之间就会产生不一致的情况。

为了确保异步方法能够正确地参与事务管理,可以通过设置@Async注解的事务传播行为属性(@Transactionalpropagation属性值)来解决这个问题。

@Transactional(propagation = Propagation.REQUIRES_NEW)  
@Async  
public void asyncMethod() {  
    System.out.println("Async method executed in thread: " + Thread.currentThread().getName());  
    // 具体业务
}

这里通过设置Propagation.REQUIRES_NEW,指示Spring在执行异步方法时开启一个新的、与当前事务无关的事务。这样即使异步方法内部发生异常,它自己的事务会独立进行提交或回滚,从而保证了数据的一致性。不过要注意的是,这种做法可能会增加系统资源消耗,因为每次异步任务都会创建新的事务上下文。

总结

通过本文的介绍,我们了解了SpringBoot@Async注解的原理、使用方法以及需要注意的事项。

@Async注解能够将方法标记为异步执行,利用了Spring框架的AOP和任务执行器机制,使得异步方法能够在后台线程池中并发执行,提高系统的并发能力和响应性。

然而,在使用@Async注解时,需要注意避免异步方法之间相互调用,合理配置线程池,进行异常处理,处理上下文丢失以及与事务的正确交互。这些注意事项能够确保异步方法的可靠性和稳定性,提高应用程序的性能和可维护性。

总的来说,@Async注解是SpringBoot中用于实现异步方法的重要特性,能够有效地提升应用程序的性能和并发能力,但在使用时需要谨慎考虑其使用场景和注意事项,以充分发挥其优势。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2024年03月11日 0

暂无评论

推荐阅读
J6X4jGS7eKRb