likes
comments
collection
share

SpringBoot 中的异步执行 @Async 和 @EnableAsync

作者站长头像
站长
· 阅读数 5

在网络开发中,请求的响应时间很重要。当我们异步执行那些耗时的任务时,可以显著提升用户体验。在 Spring 框架中,异步执行不仅是可能的,而且由于 @Async 和 @EnableAsync 等注解而得到简化。

在这篇文章中,我们将了解如何有效地利用这些注解在 Spring 应用程序中编写非阻塞并发代码。

理解异步执行

在网络和应用程序开发过程中,效率和速度通常至关重要。这就是异步执行发挥作用的地方。异步执行,可以使应用程序可以同时处理多个操作而不会受到单个耗时任务的拖累。

同步和异步

在深入了解异步执行之前,我们先了解一下同步执行。

在同步执行中,每个任务按顺序运行。这意味着任务 B 在任务 A 完成之前无法启动。这类似于逐行阅读一本书——在读完前一行之前,你不会开始阅读下一行。在 Web 应用程序中,这可以表现为在拿到处理结果之前等待数据库查询完成,然后发送短信通知。

另一方面,异步执行就像开始从书中读取一行,不会等待结果返回,又开始读取另一行。您同时管理多个线程(或任务)。在 Web 应用程序的上下文中,您可能会启动数据库查询,并在等待其完成时开始发送电子邮件。数据库查询和邮件发送是独立的,可以同时处理。

异步执行的好处

  • 非阻塞操作:异步执行最显著的优点是它不会阻塞后续任务。如果一项任务需要很长时间才能完成,其他任务不一定会排队等待。这在处理 I/O 操作等场景中尤其重要,因为等待可能会导致明显的延迟。
  • 资源效率:由于任务不必等待其他任务完成,因此异步模型中的资源利用通常更有效。线程或进程不会不必要地空闲。
  • 可扩展性:异步应用程序通常可以同时处理更多用户或任务,因为它们不会受到顺序处理的瓶颈。这使得它们更具可扩展性,尤其是在高需求的情况下。

底层机制

异步执行背后的机制通常就是事件轮询和回调机制(或某些语言中的 Promise/Futures)

事件轮询:事件轮询的核心是不断检查是否有任务要执行。如果有一个任务正在等待(例如完成的数据库查询),事件循环会拿到它并执行它关联的回调。

回调:回调是作为参数传递给稍后执行的其他函数的函数。它们通常用于确保异步操作完成后立即执行特定操作。

想象一下你正在做饭。您将一壶水放在炉子上煮沸(异步任务),并设置一个计时器(回调)以在准备好时发出蜂鸣声。当水沸腾时,您可以自由地切蔬菜或做其他事情。计时器确保你不会忘记烧开水,整个过程变得高效。

异步执行的挑战

异步执行并非没有挑战

  • 复杂性:管理多个并发任务可能会非常复杂,尤其是当任务依赖于其他任务的结果时。
  • 异常处理:在异步环境中处理异常可能很棘手,因为异常可能并不能直接显示
  • 资源管理:尽管异步执行可以更高效,但如果管理不当,可能会导致资源耗尽,尤其是在同时启动太多任务的情况下。

Spring中的@Async和@EnableAsync

Spring框架一直强调简化开发人员的开发过程。随着应用程序的发展需要更多的并发性和非阻塞操作,Spring 引入了注解,以声明式注解的方式使用异步。

@Async

Spring中的@Async注释是一种声明性方式,表示方法应该异步运行,即在单独的线程中运行,允许调用者继续执行而无需等待被调用方法的完成。

当您使用 @Async 注解一个方法时,Spring 在背后执行以下操作:

  1. 它在启动时会给调用类创建一个代理
  2. 每当调用带有 @Async 注解的方法时,Spring 都会确保方法执行是由线程池中的线程执行的,从而允许调用方法毫不延迟地继续进行。

示例

思考一下 Web 应用程序中的通知服务,您希望在不让用户等待的情况下发送通知

@Service
public class NotificationService {
    
    @Async
    public void sendNotification(User user, String message) {
        // 发送通知
        System.out.println("Notification sent to " + user.getName());
    }
}

在上面的代码中,当调用sendNotification方法时,系统不会等待消息真正发送出去。它会立即执行下一个操作,从而缩短响应时间。

使用 @EnableAsync

@Async 指示哪些方法应该异步运行,而 @EnableAsync 注释使 Spring 能够检测和处理 @Async 注解

此注解通常声明在配置类中并执行一些基本操作:

  • 触发对声明了 @Async 注解的方法的加载,并为这些类创建代理对象。
  • 允许开发人员自定义处理异步操作的执行器,确保应用程序有效扩展,而不仅仅是无差别地生成线程。

示例

在 Spring 应用程序中使用异步功能:

@Configuration
@EnableAsync
public class AppConfiguration {
    // Other bean definitions and configurations
}

@EnableAsync 注释至关重要。如果没有它,@Async 注释将被忽略,并且应用程序将同步运行

自定义异步操作

虽然 @Async 和 @EnableAsync 的基本设置很简单,但 Spring 提供了许多自定义选项:

自定义线程池:默认情况下,Spring 使用 SimpleAsyncTaskExecutor 为每个任务生成一个新线程。然而,在生产场景中,开发人员可能希望使用其他线程池来限制资源利用率并提高性能。

异常处理:异步方法的调用在异常处理方面有很大挑战。该框架提供了捕获和处理异步方法抛出的异常的方法,保证了应用程序的稳定性和可靠性。

返回类型:虽然异步方法通常具有 void 返回类型,但 Spring 还支持返回 Future 类型的方法。这允许调用者获得future结果,从而为异步操作提供更大的灵活性。

配置@Async线程池

Spring 的 @Async 注解开箱即用,使用 SimpleAsyncTaskExecutor线程池。虽然这对于基本应用程序来说可能足够了,但大多数生产级应用程序需要更受控制的线程管理方法。

为什么定制线程池

默认的 SimpleAsyncTaskExecutor 为每个任务创建一个全新的线程,没有任何限制。在并发执行大量任务的情况下,这可能会导致系统资源耗尽。

使用受控线程池需要满足以下条件:

  • 资源管理:限制并发线程数。
  • 排队:当所有线程都在运行,新任务排队等待。
  • 线程重用:减少线程创建和销毁的开销。

使用ThreadPoolTaskExecutor

ThreadPoolTask​Executor 可以对线程创建和管理进行细粒度控制。

配置方法如下:

@Configuration
@EnableAsync
public class AppConfiguration implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); // 核心线程数
        executor.setMaxPoolSize(10); // 最大线程数
        executor.setQueueCapacity(25); // 阻塞队列长度
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        return executor;
    }
}

CorePoolSize:这是执行程序要保留在池中的线​​程的基本数量。如果活动线程少于次数,新任务将始终启动新线程。

MaxPoolSize:池中线程数量的上限。如果任务不断进入并且所有核心线程都繁忙,则将创建新线程直至达到此限制。

QueueCapacity:在达到MaxPoolSize之前,任务将被放入队列中。该队列将保存任务,直到有线程可用。

其他线程池

ThreadPoolTask​​Executor 通常与 Spring 一起使用,同时也支持其他线程池。您可以选择集成 Java 并发框架中的线程池,例如 ScheduledThreadPoolExecutor 或第三方库,具体取决于您的具体需求。

优雅停止

在处理异步任务时,如何优雅地停止线程至关重要。当应用程序开始关闭时,任务可能仍在运行。配置线程池参数来处理此类情况可确保线程不会立即终止。

对于ThreadPoolTaskExecutor,你可以这样做

executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(300);

这可确保执行程序在停止之前等待指定的时间以完成任务。

处理异步方法中的异常

挑战

在同步方法中,异常处理很简单:使用 try-catch 块。当使用@Async异步执行方法时,它在单独的线程中运行。如果发生异常,调用线程不知道。这可能会导致静默,就是发生错误但未正确检测或处理。

返回 Future

处理异步方法中的异常的一种方法是让它们返回 Future ,调用方法就可以检查异常。

你可以这样做

@Async
public Future<String> asyncMethodWithException() throws Exception {
    throw new Exception("This is an asynchronous method error!");
    return new AsyncResult<>("Async response");
}

这样处理异常

Future<String> future = asyncMethodWithException();

try {
    future.get();
} catch (ExecutionException e) {
    Throwable asyncException = e.getCause();
    // 在这里处理异常
}

当调用 future.get() 时,如果异步方法抛出了异常,它将被包装在 ExecutionException 中。然后,您可以使用 e.getCause() 拿到并处理异常。

使用@Async的异常处理机制

Spring提供了一个 AsyncUncaughtExceptionHandler来处理@Async注解的方法抛出的异常。

要使用它,必须:

  1. 创建一个实现了 AsyncUncaughtExceptionHandler 的自定义异常处理类。
  2. 重写handleUncaughtException方法。
  3. 在您的配置中注册你写的自定义处理程序。

下面是:

  1. 创建自定义异常处理handler
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
        log.info("Async error in method: " + method.getName());
        throwable.printStackTrace();
    }
}
  1. 注册异常handler
@Configuration
@EnableAsync
public class AppConfiguration implements AsyncConfigurer {

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

通过此设置,每当在 @Async 注解的方法中引发异常时,都会调用 CustomAsyncExceptionHandler 的 handleUncaughtException 方法。所有异步方法进行集中异常处理

总结

异步执行在增强 Web 应用程序的性能和用户体验方面的作用是不可否认的。借助 Spring 的 @Async 和 @EnableAsync 注释,将异步方法集成到程序中比以往更容易。当我们使用时,请记住配置适合的线程池以获得最佳性能,并实现强大的异常处理功能。