Spring Boot 异步并发实现原理详解

20 阅读8分钟

目录

  1. 概述
  2. 核心原理:基于 Spring AOP 的代理机制
  3. @EnableAsync 与 @Async 的工作流程
  4. 线程池(TaskExecutor)的选择与原理
  5. 返回值处理:Future / CompletableFuture
  6. 异常处理机制
  7. 常见陷阱与失效场景
  8. 完整示例代码
  9. 与其他并发方案的对比
  10. 生产环境最佳实践

1. 概述

Spring Boot 的异步并发能力主要通过 @EnableAsync + @Async 注解实现。其本质是:

Spring 在 Bean 初始化时,对带有 @Async 注解的方法所属的类生成一个动态代理,方法被调用时不直接执行业务逻辑,而是将其包装成一个 Runnable/Callable 任务,提交给线程池(TaskExecutor)异步执行。

整个过程涉及到三个关键技术点:

  • Spring AOP / 动态代理(JDK Proxy 或 CGLIB)
  • BeanPostProcessor 机制AsyncAnnotationBeanPostProcessor
  • JUC 线程池ThreadPoolTaskExecutor 包装的 ThreadPoolExecutor

2. 核心原理:基于 Spring AOP 的代理机制

2.1 整体架构图

┌─────────────────────────────────────────────────────────────┐
│                    Caller (调用者)                            │
└──────────────────────────┬──────────────────────────────────┘
                           │ 调用 service.doAsync()
                           ▼
┌─────────────────────────────────────────────────────────────┐
│      Proxy (代理对象,由 Spring 容器返回)                     │
│      - JDK Dynamic Proxy(接口)                             │
│      - CGLIB Proxy(无接口)                                  │
└──────────────────────────┬──────────────────────────────────┘
                           │ 拦截方法调用
                           ▼
┌─────────────────────────────────────────────────────────────┐
│   AnnotationAsyncExecutionInterceptor                        │
│   (MethodInterceptor 拦截器)                                  │
│   1. 解析 @Async 注解值,确定要使用的 Executor               │
│   2. 将方法封装为 Callable<T>                                 │
│   3. 提交到 TaskExecutor                                      │
│   4. 立即返回 Future / CompletableFuture / null              │
└──────────────────────────┬──────────────────────────────────┘
                           │ executor.submit(task)
                           ▼
┌─────────────────────────────────────────────────────────────┐
│   ThreadPoolTaskExecutor (线程池)                             │
│   ├─ 核心线程数 corePoolSize                                  │
│   ├─ 最大线程数 maxPoolSize                                   │
│   ├─ 任务队列 queueCapacity                                   │
│   └─ 拒绝策略 RejectedExecutionHandler                       │
└──────────────────────────┬──────────────────────────────────┘
                           ▼
                  目标方法在工作线程中执行

2.2 关键类一览

类/接口作用
@EnableAsync通过 @Import(AsyncConfigurationSelector.class) 引入异步配置
AsyncConfigurationSelector根据 AdviceMode 选择 Proxy 或 AspectJ 模式
ProxyAsyncConfiguration注册 AsyncAnnotationBeanPostProcessor
AsyncAnnotationBeanPostProcessorBeanPostProcessor,扫描 Bean 并织入 Advisor
AsyncAnnotationAdvisor切面定义:Pointcut(@Async) + Advice(拦截器)
AnnotationAsyncExecutionInterceptor真正的拦截逻辑:包装任务并提交到 Executor
TaskExecutor / AsyncTaskExecutorSpring 抽象的执行器接口
ThreadPoolTaskExecutorSpring 对 java.util.concurrent.ThreadPoolExecutor 的包装

3. @EnableAsync 与 @Async 的工作流程

3.1 启动阶段(Bean 容器初始化)

Spring Boot 启动
        │
        ▼
解析 @EnableAsync 注解
        │
        ▼
@Import(AsyncConfigurationSelector.class)
        │
        ▼
注册 ProxyAsyncConfiguration 配置类
        │
        ▼
创建 AsyncAnnotationBeanPostProcessor (单例)
        │
        ▼
对每个 Bean 调用 postProcessAfterInitialization()
        │
        ├─ 检查类或方法是否有 @Async
        │
        ├─ 命中 → 使用 ProxyFactory 创建代理对象
        │         织入 AsyncAnnotationAdvisor
        │
        └─ 未命中 → 返回原 Bean

3.2 运行阶段(方法调用)

AnnotationAsyncExecutionInterceptor#invoke 的核心伪代码:

public Object invoke(MethodInvocation invocation) throws Throwable {
    // 1. 找到 @Async 标注的方法应使用哪个 Executor
    AsyncTaskExecutor executor = determineAsyncExecutor(method);

    // 2. 把目标方法包装成 Callable
    Callable<Object> task = () -> {
        try {
            Object result = invocation.proceed();
            if (result instanceof Future) {
                return ((Future<?>) result).get();
            }
            return result;
        } catch (Throwable ex) {
            handleError(ex, method, invocation.getArguments());
            throw ex;
        }
    };

    // 3. 根据返回类型分发到不同的提交方式
    return doSubmit(task, executor, method.getReturnType());
}

doSubmit 的分发逻辑:

方法返回类型提交方式调用方拿到的结果
voidexecutor.submit(Runnable)立即返回 null
Future<T>executor.submit(Callable<T>)FutureTask<T>
CompletableFuture<T>CompletableFuture.supplyAsync(..., executor)CompletableFuture<T>
ListenableFuture<T>executor.submitListenable(...)ListenableFuture<T>

4. 线程池(TaskExecutor)的选择与原理

4.1 默认行为(Spring Boot 2.1+)

Spring Boot 在 TaskExecutionAutoConfiguration自动装配了一个名为 applicationTaskExecutorThreadPoolTaskExecutor

默认参数(可通过 spring.task.execution.* 配置):

spring:
  task:
    execution:
      pool:
        core-size: 8              # 核心线程数(默认 8)
        max-size: Integer.MAX     # 最大线程数(默认无界,注意!)
        queue-capacity: Integer.MAX  # 队列容量(默认无界)
        keep-alive: 60s
      thread-name-prefix: task-

⚠️ 注意:默认队列是无界的,意味着 maxPoolSize 永远不会被触发——这是生产事故的重灾区。

4.2 Executor 选择优先级

@Async 拦截器选择 Executor 的顺序:

1. @Async("beanName") 中显式指定的名字
        │ 找不到 ↓
2. 实现了 AsyncConfigurer.getAsyncExecutor() 的 Bean
        │ 找不到 ↓
3. 容器中类型为 TaskExecutor 的唯一 Bean
        │ 找不到 ↓
4. 名字为 "taskExecutor" 的 Executor Bean
        │ 找不到 ↓
5. SimpleAsyncTaskExecutor(每次都新建线程,⚠️ 不要用于生产)

4.3 ThreadPoolTaskExecutor 的内核

ThreadPoolTaskExecutor 内部封装的就是 JUC 的 ThreadPoolExecutor

new ThreadPoolExecutor(
    corePoolSize,
    maxPoolSize,
    keepAliveSeconds, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(queueCapacity),
    threadFactory,
    rejectedExecutionHandler
);

任务提交时的 JUC 经典流程:

submit(task)
    │
    ▼
当前线程数 < corePoolSize ? ──是──► 创建新核心线程执行
    │否
    ▼
任务队列未满 ? ──────────────是──► 入队等待
    │否
    ▼
当前线程数 < maxPoolSize ? ───是──► 创建新非核心线程执行
    │否
    ▼
执行 RejectedExecutionHandler(默认 AbortPolicy 抛异常)

5. 返回值处理:Future / CompletableFuture

5.1 三种典型用法

// 1) fire-and-forget:不关心结果
@Async
public void sendEmail(String to) { ... }

// 2) Future:阻塞式获取结果(不推荐,已过时风格)
@Async
public Future<String> oldStyle() {
    return new AsyncResult<>("done");
}

// 3) CompletableFuture:现代异步编排(推荐)
@Async
public CompletableFuture<User> loadUser(Long id) {
    return CompletableFuture.completedFuture(repo.findById(id));
}

5.2 CompletableFuture 的优势

可以做非阻塞编排

CompletableFuture<User> userF = service.loadUser(1L);
CompletableFuture<List<Order>> ordersF = service.loadOrders(1L);

userF.thenCombine(ordersF, (user, orders) -> buildView(user, orders))
     .thenAccept(view -> render(view))
     .exceptionally(ex -> { log.error("err", ex); return null; });

5.3 原理细节:CompletableFuture 是怎么"装上"线程池的?

@Async 方法返回 CompletableFuture<T> 时,拦截器实际上做的是:

CompletableFuture.supplyAsync(() -> {
    // 调用真实方法,拿到内部 CompletableFuture,再 join 出值
    CompletableFuture<T> inner = (CompletableFuture<T>) invocation.proceed();
    return inner.join();
}, asyncExecutor);

所以整个异步链路使用的就是 @Async 配置的那个线程池,不会跑到 ForkJoinPool.commonPool 上去(这是没有 @Async 时直接用 CompletableFuture.supplyAsync 的默认行为)。


6. 异常处理机制

6.1 异常如何"消失"

@Async 方法返回 void

  • 异常不会抛回调用方(因为调用方早已返回)
  • 异常会被 AsyncUncaughtExceptionHandler 捕获
  • 默认实现 SimpleAsyncUncaughtExceptionHandler 仅打日志

当返回 Future / CompletableFuture

  • 异常封装在 Future 中,调用 .get() 时抛出 ExecutionException
  • 或通过 .exceptionally() / .handle() 处理

6.2 自定义全局异常处理器

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
        exec.setCorePoolSize(8);
        exec.setMaxPoolSize(32);
        exec.setQueueCapacity(200);
        exec.setThreadNamePrefix("async-");
        exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        exec.initialize();
        return exec;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> {
            log.error("Async method {} failed, params={}", method, params, ex);
            // 上报监控、报警等
        };
    }
}

7. 常见陷阱与失效场景

7.1 自调用失效(最经典的坑)

@Service
public class FooService {
    public void a() {
        b(); // ❌ this.b(),绕过代理,@Async 失效!
    }
    @Async
    public void b() { ... }
}

原因@Async 依赖 AOP 代理,this.b() 是直接走对象引用,不经过代理。

解决方案

  • 注入自己(不推荐,循环依赖隐患)
  • 拆分到另一个 Bean
  • 使用 AopContext.currentProxy()(需要 @EnableAspectJAutoProxy(exposeProxy = true)

7.2 方法不是 public

JDK 动态代理只能代理接口的 public 方法;CGLIB 不能代理 final / static / private 方法。

7.3 在 @PostConstruct 中调用 @Async 方法

@PostConstruct 执行时,BeanPostProcessor 还没完成代理织入,调用 @Async 方法不会异步。

7.4 默认线程池无界队列导致 OOM

前面提过:默认 queue-capacityInteger.MAX_VALUE,任务永远入队,maxPoolSize 形同虚设,最终 OOM。生产环境必须显式配置

7.5 上下文丢失

异步线程拿不到主线程的:

  • RequestContextHolder (ServletRequest)
  • SecurityContextHolder (Spring Security)
  • ThreadLocal (MDC 日志、租户上下文)

解决方案

  • RequestContextFilter 设置 threadContextInheritable=true
  • SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
  • 包装 TaskDecorator 在任务执行前手动复制 ThreadLocal:
exec.setTaskDecorator(runnable -> {
    Map<String,String> mdc = MDC.getCopyOfContextMap();
    RequestAttributes attrs = RequestContextHolder.currentRequestAttributes();
    return () -> {
        try {
            if (mdc != null) MDC.setContextMap(mdc);
            RequestContextHolder.setRequestAttributes(attrs);
            runnable.run();
        } finally {
            MDC.clear();
            RequestContextHolder.resetRequestAttributes();
        }
    };
});

7.6 事务传播失效

@Async 方法会运行在新线程上,与调用方的事务完全隔离@Transactional@Async 方法内仍然有效,但属于一个独立事务。


8. 完整示例代码

8.1 启用与配置

@SpringBootApplication
@EnableAsync
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}
@Configuration
public class ExecutorConfig {

    @Bean("ioExecutor")
    public Executor ioExecutor() {
        ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
        e.setCorePoolSize(16);
        e.setMaxPoolSize(64);
        e.setQueueCapacity(500);
        e.setKeepAliveSeconds(60);
        e.setThreadNamePrefix("io-");
        e.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        e.setWaitForTasksToCompleteOnShutdown(true);
        e.setAwaitTerminationSeconds(30);
        e.initialize();
        return e;
    }

    @Bean("cpuExecutor")
    public Executor cpuExecutor() {
        ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
        int cores = Runtime.getRuntime().availableProcessors();
        e.setCorePoolSize(cores);
        e.setMaxPoolSize(cores);
        e.setQueueCapacity(1000);
        e.setThreadNamePrefix("cpu-");
        e.initialize();
        return e;
    }
}

8.2 使用

@Service
public class OrderService {

    @Async("ioExecutor")
    public CompletableFuture<Order> fetchOrder(Long id) {
        // 模拟远程调用
        return CompletableFuture.completedFuture(remote.get(id));
    }

    @Async("ioExecutor")
    public CompletableFuture<User> fetchUser(Long id) {
        return CompletableFuture.completedFuture(remote.getUser(id));
    }
}

@RestController
public class OrderController {
    @Autowired OrderService svc;

    @GetMapping("/detail/{id}")
    public CompletableFuture<Detail> detail(@PathVariable Long id) {
        return svc.fetchOrder(id)
                  .thenCombine(svc.fetchUser(id), Detail::new);
        // Spring MVC 自动支持 CompletableFuture 返回,
        // 底层走 Servlet 3.1 异步 (AsyncContext),释放 Tomcat 工作线程
    }
}

9. 与其他并发方案的对比

方案适用场景优点缺点
@Async + 线程池业务级异步、解耦声明式、无侵入AOP 限制、上下文丢失
CompletableFuture 直接用同方法内并发编排灵活、链式需手动管理线程池
Reactor (Mono/Flux)全栈响应式、高并发 IO背压、非阻塞学习成本高、调试难
虚拟线程 (Loom, Java 21+)高并发同步代码写法简单、可大量并发需要 JDK 21+
@Scheduled定时任务简单不是为业务并发设计
MQ (Kafka/RabbitMQ)跨进程异步、可靠投递解耦、可重放引入中间件

10. 生产环境最佳实践

  1. 永远显式配置线程池,不要用默认的无界队列。
  2. 按场景拆分线程池:IO 密集、CPU 密集、定时任务各一个,避免互相饿死。
  3. 设置合理的拒绝策略CallerRunsPolicy 提供背压;AbortPolicy 快速失败。
  4. 启用线程池监控:暴露 activeCountqueueSizecompletedTaskCount 到 Prometheus。
  5. 优雅停机setWaitForTasksToCompleteOnShutdown(true) + setAwaitTerminationSeconds(N),避免 Pod 重启时丢任务。
  6. 传递上下文:用 TaskDecorator 复制 MDC、TraceId、Security、Tenant。
  7. 避免在 @Async 内部依赖调用方事务/请求,把它当作独立的执行单元。
  8. 优先返回 CompletableFuture 而不是 void 或老式 Future
  9. 结合 Spring MVC 的 Servlet 3 异步,让 Controller 直接返回 CompletableFuture,在等待期间释放 Tomcat 线程。
  10. JDK 21+ 可考虑虚拟线程spring.threads.virtual.enabled=true 直接把 Tomcat 与 @Async 切到虚拟线程,往往是更优解。

一句话总结

Spring Boot 的 @Async = AOP 代理拦截方法调用 + 把方法包装成任务 + 丢给 ThreadPoolTaskExecutor 执行 + Future/CompletableFuture 把结果送回调用方。理解 AOP 代理与 JUC 线程池这两个底座,所有"异步失效""任务积压""上下文丢失"的问题就都能自洽解释。