1、概念和用途
@Async是 Spring 框架提供的一个注解,用于标记一个方法,在一个单独的线程中异步执行。
这在处理一些耗时的操作(比如发送邮件、调用外部 API 等)时非常有用。
通过使用@Async,可以让这些操作在后台执行,而不会阻塞主线程,从而提高应用程序的性能和响应速度。
例如,在一个Web应用程序中,当用户提交一个订单后,可能需要发送一封确认邮件。如果使用同步方式,用户必须等待邮件发送完成后才能得到订单提交成功的响应。而使用@Async,可以让邮件发送操作在后台线程中进行,用户几乎可以立即得到订单提交成功的响应。
2、使用
2.1 启用异步支持
首先,需要在 Spring 配置类上添加@EnableAsync注解来开启异步方法执行功能。这个注解会扫描带有@Async标记的方法,并为它们创建独立的线程来执行。
示例配置类如下:
@Configuration
@EnableAsync
public class AppConfig {// 可以在这里进行其他配置,如Bean定义等
}
2.2 标记异步方法
在需要异步执行的方法上添加@Async注解。这个方法通常应该返回void或者Future类型。
如果返回void,方法执行完成后不会返回任何结果。如果返回Future,可以在之后获取异步方法的执行结果。
例如,下面是一个简单的异步方法,它模拟了一个耗时的操作:
@Service
public class MyService {@Asyncpublic void doSomethingAsync() {try {// 模拟耗时操作,这里休眠3秒Thread.sleep(30000);System.out.println("异步方法执行完成");} catch (InterruptedException e) {e.printStackTrace();}}
}
2.3 调用异步方法
可以在其他组件(如控制器、其他服务方法等)中调用这个异步方法。调用时,方法会立即返回,而实际的操作会在后台线程中执行。
例如,在一个 Spring MVC 控制器中调用上述异步方法:
@RestController
public class MyController {@Autowiredprivate MyService myService;@GetMapping("/async")public String asyncEndpoint() {myService.doSomethingAsync();return "异步操作已启动";}
}
2.4 需要异步结果时
如果异步方法需要返回一个结果,可以将方法的返回类型定义为Future。Future接口是 Java 并发包中的一部分,用于表示一个异步计算的结果。
例如,修改前面的MyService中的方法如下:
@Async
public Future<String> doSomethingAsyncWithResult() {try {// 模拟耗时操作,这里休眠3秒Thread.sleep(3000);return new AsyncResult<>("异步方法执行结果");} catch (InterruptedException e) {e.printStackTrace();return null;}
}
然后在调用这个方法的地方,可以通过Future的get方法来获取结果:
@GetMapping("/async - result")
public String asyncResultEndpoint() {try {Future<String> futureResult = myService.doSomethingAsyncWithResult();String result = futureResult.get();return result;} catch (Exception e) {e.printStackTrace();return "获取结果出错";}
}
注意:get方法会阻塞当前线程,直到异步方法执行完成并返回结果
3、注意事项
3.1 线程池
默认情况下,Spring 使用SimpleAsyncTaskExecutor来执行异步任务,这个执行器会为每个任务创建一个新的线程。在高并发场景下,这可能导致系统资源耗尽,因为创建线程是一个比较耗费资源的操作。而且过多的线程会增加上下文切换的成本,降低系统的整体性能。
例如,假设有一个 Web 应用,大量用户同时触发带有@Async注解的方法,如果不配置线程池,可能会创建大量线程,使服务器的 CPU 和内存资源被大量占用,最终导致应用程序响应缓慢甚至崩溃。
可以通过配置自定义的线程池来优化。
例如,定义一个线程池配置类:
@Configuration
public class ThreadPoolConfig {@Bean("asyncExecutor")public Executor asyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setQueueCapacity(25);executor.setThreadNamePrefix("Async - ");executor.initialize();return executor;}
}
然后在@Async注解中指定线程池名称:
@Async("asyncExecutor")
public void doSomethingAsync() {// 方法内容
}
合理设置线程池参数
- 核心线程数(CorePoolSize):这是线程池一直保持的线程数量,即使线程处于空闲状态也不会被销毁。应该根据应用程序的平均负载来设置。例如,如果应用程序通常需要同时处理 5 个异步任务,那么可以将核心线程数设置为 5。
- 最大线程数(MaxPoolSize):它定义了线程池允许创建的最大线程数量。当任务队列已满且有新任务到来时,线程池会创建新线程,直到达到最大线程数。设置时要考虑系统资源限制和任务的突发情况。如果系统资源有限,不能无限制地增加线程数。
- 任务队列容量(QueueCapacity):用于存储等待执行的任务。当线程池中的线程都在忙碌时,新任务会被放入任务队列。如果队列已满,且未达到最大线程数,才会创建新线程。队列容量的大小应该根据任务的平均处理时间和任务的产生频率来确定。
线程池的复用和管理
- 配置好的线程池可以复用线程,提高线程的利用率。通过合理设置线程池的参数,可以使线程在任务之间高效切换,减少线程创建和销毁的开销。同时,需要注意线程池的生命周期管理,在应用程序关闭时,应该正确地关闭线程池,以避免资源泄漏。
3.2 异常处理
异常不会自动传播给调用者,这是使用@Async时一个容易被忽视的问题。
当异步方法抛出异常时,异常不会像同步方法那样直接传播到调用者。这是因为异步方法在另一个线程中执行,异常在这个线程中被抛出,如果不进行特殊处理,调用者可能完全不知道异步方法出现了问题。
例如,在一个业务逻辑中,调用了一个带有@Async注解的方法来更新数据库记录,若该异步方法在执行过程中抛出了SQLException,如果没有处理这个异常,调用者可能会继续执行后续的操作,认为更新操作已经成功,从而导致数据不一致等问题。
在异步方法内部处理异常,可以在异步方法内部使用try - catch块来捕获和处理异常。这样可以在异步方法内部对异常进行记录、重试或者进行一些补救措施。
例如:
@Async
public void asyncMethod() {try {// 可能会抛出异常的代码} catch (Exception e) {// 记录异常日志logger.error("异步方法出现异常", e);// 可以在这里进行重试或者其他补救措施}
}
配置全局异步异常处理机制,如果不想在每个异步方法内部都处理异常,可以实现AsyncUncaughtExceptionHandler接口来配置全局的异步异常处理机制。这个接口有一个handleUncaughtException方法,当异步方法抛出未捕获的异常时会被调用。
例如:
@Configuration
@EnableAsync
public class AppConfig implements AsyncUncaughtExceptionHandler {// 开启异步支持的配置@Overridepublic void handleUncaughtException(Throwable ex, Method method, Object... params) {// 记录异常日志logger.error("异步方法出现未捕获异常,方法名: " + method.getName(), ex);// 可以在这里进行全局的异常处理策略,如通知管理员等}
}
3.3 同类中调用异步方法
如果在一个类中,一个方法(方法 A)调用了同一个类中的另一个带有@Async注解的方法(方法 B),默认情况下@Async注解可能不会生效。
这是因为 Spring 的代理机制导致的,方法 A 直接调用方法 B 时,实际上没有通过代理对象来调用,所以不会触发异步执行。
例如,在一个Service类中:
@Service
public class MyService {@Asyncpublic void asyncMethod() {// 异步执行的代码}public void anotherMethod() {asyncMethod(); // 这种情况下,@Async可能不会生效}
}
解决方法是将方法 B 的调用通过注入的代理对象来进行。可以通过@Autowired将当前类自己注入进来,然后通过代理对象调用方法 B。
例如:
@Service
public class MyService {@Autowiredprivate MyService self;@Asyncpublic void asyncMethod() {// 异步执行的代码}public void anotherMethod() {self.asyncMethod(); // 通过代理对象调用,@Async生效}
}
3.4 循环依赖
在使用@Async时,如果涉及到循环依赖,可能会导致应用程序启动失败或者出现异常行为。因为异步方法的代理对象创建和循环依赖的解决可能会相互冲突。
例如,有两个服务类ServiceA和ServiceB,它们相互依赖并且都有@Async注解的方法。
在这种情况下,需要仔细检查依赖注入的方式和异步方法的使用,避免出现循环依赖导致的问题。可以通过调整依赖注入的顺序、使用@Lazy注解等方式来缓解循环依赖问题。