深入理解Spring的@Async注解:实现异步方法调用

1,966 阅读10分钟

简介

在当今高速发展的应用开发领域,对于提升系统性能和响应能力的需求越来越迫切。而异步编程作为一种解决方案,已经成为现代应用开发中的一项重要技术。本篇博客将带您深入探究 Java 中的 @Async 注解,揭示其强大的异步执行能力和精妙的实现机制。

异步编程是一种编程模式,通过将任务分解为多个子任务,并在后台或并行线程中执行这些子任务,以提高程序的性能和响应能力。

@Async 注解简介

@Async 注解是 Spring 框架提供的注解,用于将方法标记为异步执行的方法。它的作用是告诉 Spring 框架在调用被注解的方法时,将其放入线程池中异步执行,而不是阻塞等待方法的完成。

@Async 注解的工作原理是,在调用被注解的方法时,Spring 会将该方法的执行转移到线程池中的一个线程进行处理。执行完成后,方法的返回值将通过 Future 或 CompletableFuture 进行封装,以便获取方法的返回结果。

  1. @Async 注解适用于以下场景,并具有以下优势:

    • 网络请求:在处理网络请求时,可以使用 @Async 注解将请求发送和响应处理分离,提高系统的并发处理能力。
    • 耗时计算:对于需要耗费大量时间的计算任务,可以使用 @Async 注解将计算过程放在后台执行,避免阻塞主线程,提高系统的响应速度。
    • 并行处理:通过 @Async 注解,可以同时执行多个任务,将多个相互独立的任务并行处理,从而减少整体处理时间。
    • 响应能力提升:使用异步编程可以避免阻塞主线程,提高系统的并发能力和响应能力,增强用户体验。
    • 代码简化:使用 @Async 注解可以简化编程模型,将异步执行的逻辑与业务逻辑分离,使代码更清晰、易于维护。

    异步执行通过将任务分解为多个并发执行的子任务,可以充分利用系统资源,提高系统的吞吐量和并发处理能力,从而提升系统的性能和响应能力。@Async 注解简化了异步编程的实现,使开发人员能够更方便地使用异步处理机制。同时,它还可以使代码更易于阅读和维护,提高开发效率。

@Async 注解的源码解析

@Async 注解在 Spring 框架中的实现主要依赖于以下几个关键组件:

  • AsyncAnnotationBeanPostProcessor:这是一个 Bean 后置处理器,负责解析带有 @Async 注解的方法,将其包装成异步任务。
  • AsyncTaskExecutor:这是一个任务执行器,用于执行异步任务。可以通过配置来指定具体的线程池或任务调度器。
  • AsyncConfigurer:这是一个可选的接口,用于提供自定义的异步任务执行器。

在 Spring 框架中,当启用异步支持时,AsyncAnnotationBeanPostProcessor 会扫描容器中的 Bean,并检查其中的方法是否标记有 @Async 注解。如果发现带有 @Async 注解的方法,它将会将其封装成一个代理对象,并注册为一个可执行的异步任务。

当调用被 @Async 注解标记的方法时,实际上是调用了该方法的代理对象。代理对象会将方法的执行转移到线程池中的一个线程进行处理,并返回一个 Future 对象,用于获取方法的返回结果。

线程池的配置可以通过 Spring 的配置文件或编程方式进行指定。可以配置线程池的大小、线程池的类型(如固定大小线程池、缓存线程池等)以及任务调度策略等。

异步方法与事务的关系

在使用 @Async 注解标记的异步方法与事务之间存在一些关系和注意事项。

  1. 默认情况下,异步方法不受事务管理的影响。当一个带有 @Transactional 注解的方法调用一个标记为 @Async 的异步方法时,异步方法将在一个新的线程中执行,与原始方法的事务无关。
  2. 异步方法独立事务。如果希望异步方法能够参与到事务管理中,可以使用 Propagation.REQUIRES_NEW 传播行为。将异步方法设置为 @Transactional(propagation = Propagation.REQUIRES_NEW) ,这样异步方法将在新的事务中执行,与原始方法的事务隔离开来。
  3. 异步方法和事务的提交。由于异步方法是在独立的线程中执行的,与原始方法的事务是分离的。因此,异步方法中的事务提交操作不会对原始方法的事务产生影响。即使异步方法中的事务提交失败,也不会导致原始方法的事务回滚。
  4. 异步方法和事务的异常处理。异步方法中的异常默认是不会被捕获和处理的,除非在异步方法中显式地进行了异常处理。如果需要对异步方法中的异常进行处理,可以使用 AsyncUncaughtExceptionHandler 接口来自定义异常处理逻辑。

需要注意的是,使用异步方法与事务的组合可能会带来一些潜在的问题和风险,如数据不一致性、并发冲突等。在使用异步方法和事务的同时,需要仔细考虑业务需求和数据一致性的要求,确保逻辑正确性和数据完整性。

总结起来,异步方法和事务之间的关系可以通过设置事务的传播行为来调整。默认情况下,异步方法是独立于事务的,可以通过设置 Propagation.REQUIRES_NEW 传播行为使异步方法参与到事务管理中。然而,需要注意并发和数据一致性的问题,并根据具体业务需求合理使用异步方法和事务的组合。

Async异常处理

在使用 @Async进行异步方法调用时,异常处理是一个重要的方面。以下是异步方法的异常处理机制:

  1. 默认情况下,异步方法的异常会被捕获并封装为Future对象(或CompletableFuture对象)。您可以通过Future.get() 方法或CompletableFuture.get() 方法获取异步任务的结果,并在调用时捕获异常。如果异步任务抛出异常,将会在调用get() 方法时重新抛出异常,您可以在调用端进行异常处理。
@Async
public CompletableFuture<String> performTask() {
    // 异步任务逻辑
}

// 调用异步方法并处理异常
CompletableFuture<String> future = myService.performTask();
try {
    String result = future.get();
    // 处理正常结果
} catch (InterruptedException | ExecutionException e) {
    // 处理异常情况
}
  1. 您还可以使用AsyncUncaughtExceptionHandler接口来处理异步方法中未捕获的异常。通过实现AsyncUncaughtExceptionHandler接口,并在AsyncConfigurer中重写getAsyncUncaughtExceptionHandler() 方法,您可以定义全局的异步异常处理逻辑。
@Configuration
    @EnableAsync
    public class AsyncConfig implements AsyncConfigurer {

        // 配置异步方法执行器
        @Override
        public Executor getAsyncExecutor() {
            // 配置任务执行器
        }

        // 配置异步方法未捕获异常处理器
        @Override
        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
            return new CustomAsyncExceptionHandler();
        }

        // 其他配置...
    }
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        // 处理异步方法未捕获的异常
        Class<?> clazz = method.getDeclaringClass();
        String message = String.format("异步方法执行失败,具体类名: %s, 方法名:%s, 异常信息: %s", clazz.getName(), method.getName(), ex);
        log.error("异步方法执行失败,具体类名: {}, 方法名:{}, 方法入参:{}, 异常信息: {}", clazz.getName(), method.getName(), Arrays.toString(params), ex.getMessage(), ex);
    }
}

在上述示例中,CustomAsyncExceptionHandler实现了AsyncUncaughtExceptionHandler接口,并实现了handleUncaughtException() 方法来处理异步方法中未捕获的异常。您可以在该方法中编写自定义的异常处理逻辑,例如日志记录、错误报警等。

通过上述异常处理机制,您可以捕获和处理异步方法中的异常,从而确保对异步任务的异常情况进行适当的处理。

ThreadLocal和Async使用问题

在工作过程中,经常遇到这个问题,系统通常会通过拦截器获取用户信息并设置到ThreadLoacl中,但是在异步方法中获取用户信息,却出现了获取到了其他用户信息的问题。 这是因为@Async注解会在异步执行方法时切换线程,而线程切换会导致ThreadLocal中的内容无法被正确传递。

解决这个问题的一种方法是使用AsyncTaskExecutor的子类,例如ThreadPoolTaskExecutor,并在配置中设置TaskDecoratorTaskDecorator可以在每次异步任务执行时对线程进行修饰,以确保ThreadLocal中的内容被正确传递。

以下是一个示例配置的代码片段:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setTaskDecorator(new ThreadLocalTaskDecorator()); // 设置TaskDecorator
        executor.initialize();
        return executor;
    }

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

    // 自定义的TaskDecorator
    private static class ThreadLocalTaskDecorator implements TaskDecorator {
        @Override
        public Runnable decorate(Runnable runnable) {
            // 保存当前线程的ThreadLocal内容
             // 获取调用线程的 traceId 和用户信息
            String traceId = ThreadLocalUtils.getTraceId();
            User user = ThreadLocalUtils.getUser();
            return () -> {
                try {
                    // 恢复之前保存的ThreadLocal内容
                     // 在子线程中设置 traceId 和用户信息
                    ThreadLocalUtils.setTraceId(traceId);
                    ThreadLocalUtils.setUser(user);
                    runnable.run();
                } finally {
                   // 清除子线程的 traceId 和用户信息
                   ThreadLocalUtils.clear();
                }
            };
        }
    }
}

在上述示例中,我们使用ThreadLocalContextHolder类来管理ThreadLocal的操作,包括设置、获取和清理ThreadLocal中的内容。

public class ThreadLocalUtils {
    private static final ThreadLocal<String> traceIdThreadLocal = new ThreadLocal<>();
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    public static String getTraceId() {
        return traceIdThreadLocal.get();
    }

    public static void setTraceId(String traceId) {
        traceIdThreadLocal.set(traceId);
    }

    public static User getUser() {
        return userThreadLocal.get();
    }

    public static void setUser(User user) {
        userThreadLocal.set(user);
    }

    public static void clear() {
        traceIdThreadLocal.remove();
        userThreadLocal.remove();
    }
}

通过使用以上的配置和ThreadLocalTaskDecorator,你可以确保在异步执行时,ThreadLocal中的用户信息能够正确传递并被获取到。

多线程池配置

如果您需要配置多个不同类型的 @Async注解,并且使用不同的线程池类型(缓存线程池和固定线程池),可以按照以下方式进行配置: 首先,创建多个线程池和相应的TaskExecutor bean。

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    // 缓存线程池
    @Bean("cachedThreadPool")
    public TaskExecutor cachedThreadPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(0); // 根据实际情况调整核心线程数
        executor.setMaxPoolSize(Integer.MAX_VALUE); // 根据实际情况调整最大线程数
        executor.setQueueCapacity(100); // 根据实际情况调整队列容量
        executor.setThreadNamePrefix("cached-thread-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setTaskDecorator(new MdcTaskDecorator()); // 设置任务装饰器
        executor.initialize();
        return executor;
    }

    // 固定线程池
    @Bean("fixedThreadPool")
    public TaskExecutor fixedThreadPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 根据实际情况调整核心线程数
        executor.setMaxPoolSize(10); // 根据实际情况调整最大线程数
        executor.setQueueCapacity(0); // 不使用队列,直接执行
        executor.setThreadNamePrefix("fixed-thread-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setTaskDecorator(new MdcTaskDecorator()); // 设置任务装饰器
        executor.initialize();
        return executor;
    }

    // 配置异步方法执行器
    @Override
    public Executor getAsyncExecutor() {
        return cachedThreadPool();
    }

    // 配置自定义的异步方法执行器,用于特定类型的异步任务
    @Bean("customAsyncExecutor")
    public Executor customAsyncExecutor() {
        return fixedThreadPool();
    }

    // 其他配置...

}

在上述示例中,我们创建了两个不同类型的线程池:cachedThreadPoolfixedThreadPool,并将它们作为TaskExecutor bean 注册到Spring容器中。

通过以上配置,您可以使用不同的线程池类型为不同类型的异步任务配置不同的执行器,并根据需求调整线程池的属性。

最佳实践和注意事项

在使用异步方法时,需要注意以下几点:

  • 异步方法应尽量保持简单和独立,不涉及复杂的事务逻辑。
  • 异步方法的执行时间应控制在合理的范围内,避免因长时间执行导致线程资源占用过多。
  • 需要考虑异步方法与其他业务逻辑的协调,确保异步方法的执行顺序和结果正确性。
  • 异步方法的并发性可能导致资源竞争和并发访问的问题,需要进行适当的并发控制和线程安全处理。