异步编程允许我们并发地执行任务,从而提升 API 的性能。从 Spring 3 开始,我们可以使用 @Async
注解来标记一个方法,使其可以被异步调用。
但是,有几个常见的错误 ❌ 可能会导致异步调用无法正确或高效地工作。
1. @Async
配置不当
为了让 @Async
注解正常工作,我们必须在配置类中添加 @EnableAsync
注解。
- • 错误姿势:
// 仅仅在 Service 方法上添加 @Async,但没有启用异步支持 @Service public class UserService { @Async public void processUser(User user) { // 异步方法实现... System.out.println("处理用户 (异步): " + user.getName() + " on thread " + Thread.currentThread().getName()); } } // 如果没有 @EnableAsync,上面的 @Async 将不会生效,方法会同步执行
- • 正确姿势:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration @EnableAsync// 1. 启用异步方法执行的支持 publicclassAsyncConfiguration { @Bean(name = "asyncExecutor")// 2. 定义一个自定义的 Executor Bean public Executor asyncExecutor() { ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor(); executor.setCorePoolSize(5); // 核心线程数 executor.setMaxPoolSize(10); // 最大线程数 executor.setQueueCapacity(25); // 队列容量 executor.setThreadNamePrefix("TestAsyncThread-"); // 线程名前缀 executor.initialize(); return executor; } // 如果定义了自定义 Executor Bean,@Async 方法默认会使用它。 // 也可以在 @Async("anotherExecutor") 中指定使用特定的 Executor。 } // UserService 中的 @Async 方法现在会由 asyncExecutor 执行 // @Service // public class UserService { // @Async("asyncExecutor") // 可以显式指定 // public void processUser(User user) { ... } // }
-
• 注意事项:
-
1. 我们需要使用
@EnableAsync
来激活对异步调用的支持。 -
2. 我们应该使用一个自定义的
Executor
(线程池) 来更有效地管理线程。默认情况下(即没有自定义Executor
Bean 时),@Async
注解会使用 Spring 的SimpleAsyncTaskExecutor
,它不是一个真正的线程池(每次调用都会创建一个新线程),在高并发下可能会导致创建过多线程,甚至引发OutOfMemoryError
(内存溢出错误)。
-
2. 异常处理不当以及忽略返回值
不对异步方法的返回值和可能抛出的异常进行妥善处理,可能会导致破坏性的代码和系统故障。
- • 糟糕的代码:
@Service publicclassReportService { @Async publicvoidgenerateReport() { // 这个方法如果发生异常,调用方无法感知,异常会静默失败 System.out.println("开始生成报告 on thread " + Thread.currentThread().getName()); if (true) { // 模拟异常 thrownewRuntimeException("报告生成失败!"); } System.out.println("报告生成成功 (理论上)"); } publicvoidtriggerReportGeneration() { System.out.println("主线程发起报告生成..."); generateReport(); // 调用异步方法,不关心其结果或异常 System.out.println("主线程已发起,继续执行其他操作..."); } }
- • 改进后的代码:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.concurrent.CompletableFuture; @Service publicclassReportService { privatestaticfinalLoggerlogger= LoggerFactory.getLogger(ReportService.class); @Async("asyncExecutor")// 假设我们用了前面定义的 asyncExecutor public CompletableFuture<String> generateReport() { logger.info("异步开始生成报告..."); try { // 模拟报告生成逻辑 Thread.sleep(2000); // 模拟耗时操作 if (Math.random() < 0.5) { // 模拟可能发生的失败 thrownewRuntimeException("模拟的报告生成内部错误!"); } StringreportContent="报告内容:一切正常"; logger.info("异步报告生成成功。"); return CompletableFuture.completedFuture(reportContent); // 返回包含成功结果的 Future } catch (Exception e) { logger.error("异步报告生成失败。", e); return CompletableFuture.failedFuture(e); // 返回包含异常的 Future } } publicvoidprocessReport() { logger.info("主线程发起报告生成并处理结果..."); CompletableFuture<String> reportFuture = generateReport(); // 异步处理成功或失败的结果 reportFuture.thenAccept(reportResult -> { logger.info("主线程收到异步报告结果: {}", reportResult); // 在这里处理成功生成的报告 }).exceptionally(ex -> { // 处理异步方法中的异常 logger.error("主线程捕获到异步报告生成失败的异常: {}", ex.getMessage()); // 可以在这里执行备用逻辑,比如返回一个默认报告或错误提示 return"生成报告时发生错误,请稍后再试。"; // exceptionally 需要一个返回值 }); logger.info("主线程已发起报告生成,可以继续做其他事情,结果将异步处理。"); } }
-
• 要点:
-
• 为了有效地处理返回值和异常,我们可以让
@Async
方法返回CompletableFuture<T>
。我已经写过一篇关于 CompletableFuture 的文章 (原文提示,此处应有链接)。 -
• 对异常进行恰当的日志记录。
-
• 调用方可以使用
CompletableFuture
的thenAccept()
,thenApply()
,exceptionally()
,handle()
等方法来处理异步结果或异常。
-
3. 在异步方法中执行阻塞操作
有时,我们会在异步代码内部引入阻塞性逻辑,这完全违背了使用异步方法的初衷(即释放调用者线程,提高吞吐量)。如果异步线程池中的线程也因为阻塞操作而长时间被占用,那么异步处理的优势就荡然无存了。
- • 阻塞的代码:
@Service publicclassDataProcessingService { // @Autowired private DataRepository dataRepository; // 假设注入了 // public void heavyProcessingMethod(Data item) { try{Thread.sleep(100);}catch(Exception e){} } @Async publicvoidprocessData() { logger.info("异步开始处理数据..."); // 1. 阻塞式的数据库调用 List<Data> dataList = dataRepository.findAll(); // 如果 findAll() 是阻塞的,这里会阻塞异步线程 // 2. 同步的、阻塞式的循环处理 for (Data item : dataList) { heavyProcessingMethod(item); // 如果这个方法是耗时的CPU密集型或阻塞I/O,同样会阻塞异步线程 } logger.info("异步数据处理完成 (阻塞方式)。"); } }
- • 非阻塞的实现 (概念性示例):
(要点:尽量避免在import java.util.stream.Collectors; // ... 其他 import @Service publicclassDataProcessingService { // @Autowired private DataRepository dataRepository; // 假设注入了 // public Data processDataItem(Data item) { item.process(); return item;} // 假设 Data 有 process 方法 @Async("asyncExecutor") public CompletableFuture<Void> processDataNonBlocking() { logger.info("异步开始非阻塞处理数据..."); // 使用 CompletableFuture.supplyAsync 将整个操作提交到另一个线程池 (或者当前异步线程池) // 并且如果 dataRepository 支持响应式/异步查询,效果更佳 return CompletableFuture.runAsync(() -> { // 或者 supplyAsync 如果需要返回结果 List<Data> dataList = dataRepository.findAll(); // 理想情况下,这里应该是异步获取数据 // 对于数据处理部分,可以考虑使用并行流 (parallelStream) 来加速CPU密集型任务 // 注意:并行流使用的是 ForkJoinPool,不是我们配置的 asyncExecutor // 如果 processDataItem 是 I/O 密集型,可能需要更复杂的异步编排 dataList.stream() .parallel() // 尝试并行处理 .map(this::processDataItem) // 调用处理单个数据项的方法 .collect(Collectors.toList()); // 收集结果 (如果需要的话) logger.info("异步数据处理完成 (尝试非阻塞/并行方式)。"); }, asyncExecutor); // 可以指定执行器 } private Data processDataItem(Data item) { // 理想情况下,这里的处理也应该是异步/非阻塞的, // 或者至少是可以在并行流中安全高效执行的。 logger.debug("处理数据项: {}", item.getId()); return item.process(); // 假设 item.process() 是实际处理逻辑 } }
@Async
方法中直接进行长时间的阻塞I/O。如果可能,将I/O操作也异步化,或者将其委托给专门的I/O线程池。对于CPU密集型任务,可以考虑在异步方法内部使用并行流等技术进一步优化。)
4. 事务管理使用不当
当 @Transactional
和 @Async
一起使用时,需要特别小心,因为它们可能不会像你期望的那样工作。默认情况下,事务上下文是与线程绑定的,异步方法在新线程中执行,可能不会继承调用方线程的事务上下文。
- • 错误姿势 (可能导致事务问题):
@Service publicclassUserService { // @Autowired private UserRepository userRepository; @Transactional// 事务注解 @Async // 异步注解,这两个注解直接用在同一个方法上时要特别小心 publicvoidupdateUserStatus(Long userId) { // 事务可能不会按预期工作。 // 如果 @Async 的代理先生效,则 @Transactional 可能在错误的线程或时机作用,甚至无效。 // 如果 @Transactional 的代理先生效,那么异步执行的部分可能在事务提交后才开始,或者在新线程中没有事务。 logger.info("异步尝试更新用户 {} 状态 (可能有事务问题)", userId); Useruser= userRepository.findById(userId).orElseThrow(() -> newRuntimeException("User not found")); user.setStatus(UserStatus.ACTIVE); userRepository.save(user); logger.info("用户 {} 状态已更新 (可能有事务问题)", userId); } }
- • 正确的代码 (将事务与异步分离):
(要点:通常,最佳实践是将事务边界控制在同步方法中,然后由该同步方法去调用异步方法。异步方法如果也需要事务,应该配置为import org.springframework.transaction.annotation.Propagation; // ... 其他 import @Service publicclassUserService { // @Autowired private UserRepository userRepository; // @Autowired private UserService self; // 用于自调用代理 (或另一个Service) // 同步的、事务性的入口方法 @Transactional// 这个方法在调用者线程中,在一个事务内执行 publicvoidinitiateUserStatusUpdate(Long userId) { logger.info("发起用户 {} 状态更新流程 (事务性)", userId); // 从这里调用异步方法。通常需要通过代理调用才能使 @Async 生效。 // 如果 updateUserStatusAsync 在同一个类中,直接调用 this.updateUserStatusAsync() // 可能不会触发 @Async 的代理。推荐将其放在另一个 Service 类中,或者注入自身代理。 // self.updateUserStatusAsync(userId); updateUserStatusAsync(userId); // 简化示例,假设代理能正确工作或在不同类中 } // 异步方法,可以有自己的事务配置 @Async("asyncExecutor") @Transactional(propagation = Propagation.REQUIRES_NEW)// 异步方法通常需要开启一个新的事务 publicvoidupdateUserStatusAsync(Long userId) { logger.info("异步开始更新用户 {} 状态 (新事务)", userId); Useruser= userRepository.findById(userId).orElseThrow(() -> newRuntimeException("User not found")); user.setStatus(UserStatus.ACTIVE); userRepository.save(user); logger.info("用户 {} 状态已异步更新 (新事务)", userId); } }
Propagation.REQUIRES_NEW
来开启一个独立的、新的事务。)
5. 从同一个类中调用异步方法
@Async
注解的异步功能是通过 Spring AOP 代理机制来实现的。这意味着,如果你在同一个类的一个普通方法中,直接通过 this
关键字调用该类中另一个标记了 @Async
的方法,那么异步效果将会失效(因为调用没有经过代理对象)。
- • 错误姿势:
@Service publicclassTestService { // @Autowired private AsyncService asyncService; // 应该注入另一个服务 publicvoidsomeSyncMethodThenAsync() { logger.info("同步方法开始..."); // 一些逻辑... this.internalAsyncMethod(); // 错误!通过 this 调用,@Async 不会生效,会变成同步调用 logger.info("同步方法结束 (可能在 internalAsyncMethod 执行完后才结束)。"); } @Async("asyncExecutor") publicvoidinternalAsyncMethod() { logger.info("internalAsyncMethod 正在执行 on thread " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) {} logger.info("internalAsyncMethod 执行完毕。"); } }
- • 正确的实现 (将异步方法移到另一个 Bean 中):
(要点:要确保// 主服务类 @Service publicclassTestService { @Autowired private AsyncHelperService asyncHelperService; // 注入包含异步方法的辅助 Bean publicvoidtriggerAsyncCall() { logger.info("TestService: 发起异步调用..."); asyncHelperService.performAsyncTask(); // 通过代理调用,@Async 生效 logger.info("TestService: 异步调用已发起,主流程继续..."); } } // 包含异步方法的辅助 Service 类 @Service publicclassAsyncHelperService { @Async("asyncExecutor") publicvoidperformAsyncTask() { logger.info("AsyncHelperService: performAsyncTask 正在执行 on thread " + Thread.currentThread().getName()); try { Thread.sleep(2000); // 模拟耗时操作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } logger.info("AsyncHelperService: performAsyncTask 执行完毕。"); } }
@Async
生效,调用异步方法的请求必须通过 Spring 创建的代理对象。最简单的方法是将异步方法放在一个单独的 Bean 中,然后通过依赖注入来调用它。)
以上就是一些我们在使用 @Async
注解时常犯的错误。还有其他一些情况也容易被忽略,比如异步方法的调用顺序不当,这可能导致结果的随机性或不确定性,需要根据具体业务场景仔细设计。