第一部分:异步编程------高并发系统的"消峰填谷"
你必须理解异步的本质:非阻塞(Non-blocking)。
在传统的同步调用中,线程会卡在耗时操作(如发邮件、调用第三方接口)上,导致吞吐量急剧下降。而异步则是将这些操作丢给另一个线程,主线程直接返回。
1. Spring @Async 的极简体验
在 Spring Boot 中,异步只需两步:
-
开启支持 :启动类加
@EnableAsync。 -
标记异步 :方法上加
@Async。
第二部分:底层逻辑------@Async 的动态代理黑盒
面试官:"你能从源码层面讲讲 @Async 是怎么生效的吗?"
1. 代理的诞生:BeanPostProcessor
@Async 的本质是 AOP(面向切面编程) 。Spring 通过 AsyncAnnotationBeanPostProcessor 这个后置处理器,在 Bean 初始化的最后阶段,检查类或方法上是否有 @Async。如果有,则为其创建一个 代理对象。
2. 拦截与分发:AnnotationAsyncExecutionInterceptor
当外部调用该代理对象的方法时,拦截器会介入:
-
任务封装 :将原始方法的执行逻辑封装成一个
Callable或Runnable。 -
线程池调度:从配置的线程池(TaskExecutor)中取出一个线程来执行这个任务。
-
即时返回 :如果是
void或Future,拦截器会立即给调用者返回一个结果(或空的 Future)。
第三部分:避坑指南------那些让异步失效的"名场面"
在大厂面试中,我最常考查"失效场景",这能看出你是否真的理解代理机制。
1. 方法自调用(Internal Call)
失效代码示例:
Java
@Service
public class OrderService {
public void processOrder() {
// 直接调用本类异步方法,不走代理对象,异步失效!
this.sendEmailAsync();
}
@Async
public void sendEmailAsync() {
// 耗时操作
}
}
底层逻辑 :AOP 增强是基于代理对象的。通过 this 调用是目标对象内部的直接调用,根本没有经过代理层。
2. 访问修饰符限制
@Async 标记的方法必须是 public。Spring 的代理机制(尤其是 JDK 动态代理)无法拦截非 public 方法。
3. 未开启异步支持
低级错误:忘记在配置类上标注 @EnableAsync。
第四部分:线程池管理------@Async 的核心心脏
面试官:"默认情况下,@Async 使用哪个线程池?有什么风险?"
1. 默认线程池的隐患
默认情况下,Spring 使用 SimpleAsyncTaskExecutor。
- 风险点 :它不重用线程,而是每次调用都开启新线程!在高并发下,这会导致系统资源耗尽(OOM)。
2. 大厂标配:自定义线程池
作为资深工程师,你必须手动配置 ThreadPoolTaskExecutor,并赋予清晰的业务名称。
Java
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程
executor.setMaxPoolSize(50); // 最大线程
executor.setQueueCapacity(100); // 缓冲队列
executor.setThreadNamePrefix("Order-Async-"); // 方便排查日志
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
executor.initialize();
return executor;
}
}
第五部分:异步结果的处理------Future 与 CompletableFuture
如果异步方法有返回值,应该如何优雅处理?
1. CompletableFuture 的力量
相较于旧的 Future,CompletableFuture 支持非阻塞的回调链。
Java
@Async
public CompletableFuture<String> fetchUserDetail(String userId) {
// 模拟耗时查询
return CompletableFuture.completedFuture("UserDetail: " + userId);
}
// 调用端:非阻塞处理
fetchUserDetail("123").thenAccept(result -> {
log.info("处理完成: {}", result);
});
第六部分:异常处理------当异步任务报错时
异步任务在子线程执行,主线程无法通过 try-catch 捕获其异常。
1. 自定义异常处理器
通过实现 AsyncUncaughtExceptionHandler,我们可以统一收集并监控异步任务中的错误。
Java
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("异步任务执行失败!方法: {}, 错误信息: {}", method.getName(), ex.getMessage());
// 发送告警通知
}
}
第七部分:面试复盘脑图
Code snippet
mindmap
root((Spring @Async 原理))
实现机制
AOP 代理: BeanPostProcessor 生成代理对象
拦截器: AnnotationAsyncExecutionInterceptor
线程池: 将任务提交给 TaskExecutor
失效场景
自调用: 绕过代理对象
非 Public: 代理无法切入
未开启: 缺少 @EnableAsync
线程池策略
默认: SimpleAsyncTaskExecutor (不重用线程, 易OOM)
自定义: ThreadPoolTaskExecutor (参数调优)
拒绝策略: CallerRunsPolicy 保证任务不丢失
结果与异常
返回值: Future, CompletableFuture
异常捕获: AsyncUncaughtExceptionHandler
事务影响
事务隔离: 异步方法在独立线程, 无法共享主线程事务
解决方案: 异步方法内部开启新事务
第八部分:大厂面试官的"深度思考题"
-
如果在 @Async 方法上加了 @Transactional,事务会生效吗?
- 回答要点 :会生效,但它是独立的事务。因为异步方法在不同的线程中,无法感知主线程的事务上下文。如果你需要异步执行且保证事务,必须在异步方法内部管理事务。
-
异步方法执行的顺序性能保证吗?
- 回答要点 :不能。异步任务的执行顺序取决于线程池的调度和操作系统的 CPU 分配。如果业务有严格顺序要求,建议使用
CompletableFuture的链式调用或消息队列。
- 回答要点 :不能。异步任务的执行顺序取决于线程池的调度和操作系统的 CPU 分配。如果业务有严格顺序要求,建议使用
-
如何监控异步线程池的状态?
- 回答要点 :通过
ThreadPoolTaskExecutor暴露出的getPoolSize()、getActiveCount()等方法,配合 Spring Boot Actuator 或 Prometheus 进行打点监控。
- 回答要点 :通过
结语:从"可用"到"可靠"
异步不是万能药,它是对系统复杂度的交换。
掌握了 @Async 的底层代理机制、线程池调优以及异常处理,你才能在复杂的分布式系统中游刃有余。