提升Spring Boot应用性能的秘密武器:揭秘@Async注解的实用技巧
弁言在一样平常业务开发中,异步编程已成为应对并发挑战和提升应用程序性能的关键策略。传统的同步编程方式,由于会阻碍主线程执行后续任务直至程序代码执行结束,不可避免地降低了程序整体效率与响应速率。因此,为克服这一瓶颈,开发者广泛采用异步编程技能,将那些可能阻塞的长时间运行任务委派至背景线程处理,从而确保主线程始终保持高效和灵敏的响应本领。
而SpringBoot作为一款广受欢迎的应用开发框架,极大地简化了异步编程实践。此中,@Async注解是SpringBoot为实现异步编程提供的便捷工具之一。通过巧妙地应用@Async注解,开发者能够无缝地将方法调用转化为异步执行模式,进而增强体系的并发性能表现。
本文将深度剖析SpringBoot中的@Async注解,包括其内在原理、具体使用方法以及相关注意事项。我们将深入探究@Async的工作机制,展示如何在实际的SpringBoot项目中有效运用该注解。
@Async的原理
在SpringBoot中,@Async注解的实现原理基于Spring框架的AOP和任务执行器(Task Executor)机制。
@Async的启用
开启对异步方法的支持需要在配置类上添加@EnableAsync注解,然后就可以激活了一个Bean后处理器:AsyncConfigurationSelector,它负责自动配置AsyncConfigurer,为异步方法提供所需的线程池。
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240227082503.png
而AsyncConfigurationSelector中默认使用PROXY的署理,纵然用ProxyAsyncConfiguration,而ProxyAsyncConfiguration是用于配置Spring异步方法的署理模式的配置类。
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240227084444.png
当然我们还可以指定使用另外一个署理模式:AdviceMode.ASPECTJ,以便使用AspectJ来进行更高级的拦截和处理。
它继续至AbstractAsyncConfiguration,在AbstractAsyncConfiguration中配置AsyncConfigurer。setConfigurers方法用于设置异步任务执行器和异常处理器。
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240227090143.png
AsyncConfigurer中提供了一种便捷的方式来配置异步方法的执行器(AsyncTaskExecutor)。通过实现AsyncConfigurer接口,可以自定义异步方法的执行策略、线程池等配置信息。默认环境下Spring会先搜索TaskExecutor范例的bean或者名字为taskExecutor的Executor范例的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管理的线程池去执行。这个过程确保了异步方法的执行不会阻塞调用者线程。
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240227185415.png
TaskExecutor与线程池
TaskExecutor是一个接口,定义了如何执行Runnable或Callable任务。SpringBoot提供了多种实现,如SimpleAsyncTaskExecutor、ThreadPoolTaskExecutor等。通常我们会自定义一个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);
}
}
}输出效果如下:
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240227220046.png
注意事项
[*]@Async必须共同@EnableAsync注解一起使用。两者缺一不可。
[*]异步方法必须定义在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);
}
}执行效果如下:
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240227221119.png
都是主线程同步方法。
[*]异步方法不能定义为private或static,因为Spring AOP无法拦截这些方法。我们修改AsyncTaskService类中的方法修改为private或者static,则会发生编译错误:
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240227221330.png
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240227221345.png
[*]异步方法内部的调用不能使用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());
}
}
[*]被@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();
}
}执行效果如下:
https://coderacademy.oss-cn-zhangjiakou.aliyuncs.com/blogcontent/20240227223539.png
[*]不同的异步方法间不要相互调用
异步方法间的相互调用会显著增长代码的复杂性层级,由于异步执行的本质在于即时返回并延迟完成任务,因此,嵌套或递归式的异步调用容易导致逻辑难以梳理和维护,特殊是在涉及多异步操作状态追踪、顺序控制及依靠关系管理时尤为突出。
当异步方法内部进一步调用其他异步方法,并且牵涉到同步资源如锁、信号量等时,极易引发死锁问题。例如,一个线程在等待自身启动的另一个异步任务效果的同时,该任务却尝试获取第一个线程所持有的锁,云云循环等待,形成无法解开的死锁。
无控制地在异步方法内部启动新的异步任务,特殊是在高并发场景下,可能导致线程池资源迅速耗尽,使得体系丧失处理更多请求的本领。此外,直接的异步方法调用还增长了错误处理与日志记录的难度,特殊是碰到异常环境时,每每难以追溯原始调用链路以精准定位问题源头。
若需要确保异步方法按照特定顺序执行,直接调用会导致逻辑杂乱不清。为解决这一问题,通常保举采用回调机制、Future/CompletionStage链式编程、响应式编程模型(如RxJava、Project Reactor)等方式来确保有序执行并降低耦合度。
同时,频繁且低延迟的任务间直接相互调用可能会引入额外的上下文切换开销,从而对体系的整体性能造成潜在负面影响。
[*]合理配置线程池
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线程池最全详解
[*]异常处理:
异步方法内部的异常通常不会被调用方捕获到,因此需要在异步方法内部进行异常处理,可以通过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);
// 其他错误处理逻辑
}
}
[*]与事务的交互
默认环境下,当我们在Spring应用中使用@Async注解标志一个方法为异步执行时,这个方法将不会参与到其调用者所处的事务上下文中。这意味着,如果调用异步方法的方法在一个事务内执行,该事务将在调用异步方法后正常提交或回滚,但异步方法内部的操作并不会受到这个事务的影响。
例如,若在同步方法中修改了数据库记录,并随后调用了一个异步方法来更新其他相关的数据,那么如果同步方法中的事务在调用异步方法后提交,而异步方法在执行过程中抛出了异常导致更新失败,这时第一部分已提交的数据和第二部分未成功更新的数据之间就会产生不同等的环境。
为了确保异步方法能够正确地参与事务管理,可以通过设置@Async注解的事务传播举动属性(@Transactional的propagation属性值)来解决这个问题。
@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、中心件、架构设计、面试题、程序员攻略等
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]