Java Spring的@Async的利用及注意事项

打印 上一主题 下一主题

主题 847|帖子 847|积分 2541

1、概念和用途

@Async是 Spring 框架提供的一个注解,用于标记一个方法,在一个单独的线程中异步执行。
这在处理一些耗时的操纵(好比发送邮件、调用外部 API 等)时非常有用。
通过利用@Async,可以让这些操纵在后台执行,而不会壅闭主线程,从而提高应用程序的性能和相应速度。
比方,在一个Web应用程序中,当用户提交一个订单后,可能需要发送一封确认邮件。如果利用同步方式,用户必须等待邮件发送完成后才能得到订单提交乐成的相应。而利用@Async,可以让邮件发送操纵在后台线程中进行,用户几乎可以立即得到订单提交乐成的相应。
2、利用

2.1 启用异步支持

起首,需要在 Spring 配置类上添加@EnableAsync注解来开启异步方法执行功能。这个注解会扫描带有@Async标记的方法,并为它们创建独立的线程来执行。
示例配置类如下:
  1. @Configuration
  2. @EnableAsync
  3. public class AppConfig {
  4.     // 可以在这里进行其他配置,如Bean定义等
  5. }
复制代码
2.2 标记异步方法

在需要异步执行的方法上添加@Async注解。这个方法通常应该返回void大概Future类型。
如果返回void,方法执行完成后不会返回任何效果。如果返回Future,可以在之后获取异步方法的执行效果。
比方,下面是一个简单的异步方法,它模拟了一个耗时的操纵:
  1. @Service
  2. public class MyService {
  3.     @Async
  4.     public void doSomethingAsync() {
  5.         try {
  6.             // 模拟耗时操作,这里休眠3秒
  7.             Thread.sleep(30000);
  8.             System.out.println("异步方法执行完成");
  9.         } catch (InterruptedException e) {
  10.             e.printStackTrace();
  11.         }
  12.     }
  13. }
复制代码
2.3 调用异步方法

可以在其他组件(如控制器、其他服务方法等)中调用这个异步方法。调用时,方法会立即返回,而实际的操纵会在后台线程中执行。
比方,在一个 Spring MVC 控制器中调用上述异步方法:
  1. @RestController
  2. public class MyController {
  3.     @Autowired
  4.     private MyService myService;
  5.     @GetMapping("/async")
  6.     public String asyncEndpoint() {
  7.         myService.doSomethingAsync();
  8.         return "异步操作已启动";
  9.     }
  10. }
复制代码
2.4 需要异步效果时

如果异步方法需要返回一个效果,可以将方法的返回类型定义为Future。Future接口是 Java 并发包中的一部分,用于表示一个异步盘算的效果。
比方,修改前面的MyService中的方法如下:
  1. @Async
  2. public Future<String> doSomethingAsyncWithResult() {
  3.     try {
  4.         // 模拟耗时操作,这里休眠3秒
  5.         Thread.sleep(3000);
  6.         return new AsyncResult<>("异步方法执行结果");
  7.     } catch (InterruptedException e) {
  8.         e.printStackTrace();
  9.         return null;
  10.     }
  11. }
复制代码
然后在调用这个方法的地方,可以通过Future的get方法来获取效果:
  1. @GetMapping("/async - result")
  2. public String asyncResultEndpoint() {
  3.     try {
  4.         Future<String> futureResult = myService.doSomethingAsyncWithResult();
  5.         String result = futureResult.get();
  6.         return result;
  7.     } catch (Exception e) {
  8.         e.printStackTrace();
  9.         return "获取结果出错";
  10.     }
  11. }
复制代码
注意:get方法会壅闭当前线程,直到异步方法执行完成并返回效果
3、注意事项

3.1 线程池

默认情况下,Spring 利用SimpleAsyncTaskExecutor来执行异步任务,这个执行器会为每个任务创建一个新的线程。在高并发场景下,这可能导致体系资源耗尽,因为创建线程是一个比力耗费资源的操纵。而且过多的线程会增长上下文切换的成本,低落体系的整体性能。
比方,假设有一个 Web 应用,大量用户同时触发带有@Async注解的方法,如果不配置线程池,可能会创建大量线程,使服务器的 CPU 和内存资源被大量占用,终极导致应用程序相应迟钝甚至崩溃。
可以通过配置自定义的线程池来优化。
比方,定义一个线程池配置类:
  1. @Configuration
  2. public class ThreadPoolConfig {
  3.     @Bean("asyncExecutor")
  4.     public Executor asyncExecutor() {
  5.         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  6.         executor.setCorePoolSize(5);
  7.         executor.setMaxPoolSize(10);
  8.         executor.setQueueCapacity(25);
  9.         executor.setThreadNamePrefix("Async - ");
  10.         executor.initialize();
  11.         return executor;
  12.     }
  13. }
复制代码
然后在@Async注解中指定线程池名称:
  1. @Async("asyncExecutor")
  2. public void doSomethingAsync() {
  3.     // 方法内容
  4. }
复制代码
  合理设置线程池参数
  

  • 核心线程数(CorePoolSize):这是线程池不停保持的线程数量,纵然线程处于空闲状态也不会被销毁。应该根据应用程序的平均负载来设置。比方,如果应用程序通常需要同时处理 5 个异步任务,那么可以将核心线程数设置为 5。
  • 最大线程数(MaxPoolSize):它定义了线程池允许创建的最大线程数量。当任务队列已满且有新任务到来时,线程池会创建新线程,直到达到最大线程数。设置时要思量体系资源限制和任务的突发情况。如果体系资源有限,不能无限制地增长线程数。
  • 任务队列容量(QueueCapacity):用于存储等待执行的任务。当线程池中的线程都在忙碌时,新任务会被放入任务队列。如果队列已满,且未达到最大线程数,才会创建新线程。队列容量的大小应该根据任务的平均处理时间和任务的产生频率来确定。
  线程池的复用和管理
  

  • 配置好的线程池可以复用线程,提高线程的利用率。通过合理设置线程池的参数,可以使线程在任务之间高效切换,减少线程创建和销毁的开销。同时,需要注意线程池的生命周期管理,在应用程序关闭时,应该精确地关闭线程池,以制止资源泄漏。
  3.2 非常处理 

非常不会自动传播给调用者,这是利用@Async时一个容易被忽视的问题。
当异步方法抛出非常时,非常不会像同步方法那样直接传播到调用者。这是因为异步方法在另一个线程中执行,非常在这个线程中被抛出,如果不进行特别处理,调用者可能完全不知道异步方法出现了问题。
比方,在一个业务逻辑中,调用了一个带有@Async注解的方法来更新数据库记载,若该异步方法在执行过程中抛出了SQLException,如果没有处理这个非常,调用者可能会继续执行后续的操纵,以为更新操纵已经乐成,从而导致数据不同等等问题。
在异步方法内部处理非常,可以在异步方法内部利用try - catch块来捕捉和处理非常。这样可以在异步方法内部对非常进行记载、重试大概进行一些补救措施。
比方:
  1. @Async
  2. public void asyncMethod() {
  3.     try {
  4.         // 可能会抛出异常的代码
  5.     } catch (Exception e) {
  6.         // 记录异常日志
  7.         logger.error("异步方法出现异常", e);
  8.         // 可以在这里进行重试或者其他补救措施
  9.     }
  10. }
复制代码
配置全局异步非常处理机制,如果不想在每个异步方法内部都处理非常,可以实现AsyncUncaughtExceptionHandler接口来配置全局的异步非常处理机制。这个接口有一个handleUncaughtException方法,当异步方法抛出未捕捉的非常时会被调用。
比方:
  1. @Configuration
  2. @EnableAsync
  3. public class AppConfig implements AsyncUncaughtExceptionHandler {
  4.     // 开启异步支持的配置
  5.     @Override
  6.     public void handleUncaughtException(Throwable ex, Method method, Object... params) {
  7.         // 记录异常日志
  8.         logger.error("异步方法出现未捕获异常,方法名: " + method.getName(), ex);
  9.         // 可以在这里进行全局的异常处理策略,如通知管理员等
  10.     }
  11. }
复制代码
3.3 同类中调用异步方法

如果在一个类中,一个方法(方法 A)调用了同一个类中的另一个带有@Async注解的方法(方法 B),默认情况下@Async注解可能不会见效。
这是因为 Spring 的代理机制导致的,方法 A 直接调用方法 B 时,实际上没有通过代理对象来调用,所以不会触发异步执行。
比方,在一个Service类中:
  1. @Service
  2. public class MyService {
  3.     @Async
  4.     public void asyncMethod() {
  5.         // 异步执行的代码
  6.     }
  7.     public void anotherMethod() {
  8.         asyncMethod(); // 这种情况下,@Async可能不会生效
  9.     }
  10. }
复制代码
解决方法是将方法 B 的调用通过注入的代理对象来进行。可以通过@Autowired将当前类自己注入进来,然后通过代理对象调用方法 B。
比方:
  1. @Service
  2. public class MyService {
  3.     @Autowired
  4.     private MyService self;
  5.     @Async
  6.     public void asyncMethod() {
  7.         // 异步执行的代码
  8.     }
  9.     public void anotherMethod() {
  10.         self.asyncMethod(); // 通过代理对象调用,@Async生效
  11.     }
  12. }
复制代码
3.4 循环依赖

在利用@Async时,如果涉及到循环依赖,可能会导致应用程序启动失败大概出现非常行为。因为异步方法的代理对象创建和循环依赖的解决可能会相互辩论。
比方,有两个服务类ServiceA和ServiceB,它们相互依赖并且都有@Async注解的方法。
在这种情况下,需要细致查抄依赖注入的方式和异步方法的利用,制止出现循环依赖导致的问题。可以通过调整依赖注入的次序、利用@Lazy注解等方式来缓解循环依赖问题。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

小小小幸运

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表