一、先澄清一个误区:Java 里没有 async 关键字
很多开发者搜索 java async 时,会期望找到类似 C# 中 async/await 那样的语法糖。但事实是——Java 语言本身并没有 async 关键字。C# 的 async 是上下文关键字,用于修饰异步方法,配合 await 实现协程式编程。而 Java 的异步编程,靠的是另一套完全不同的体系。
在 Java 生态中,实现异步的主流方案有七种:手动线程池(ExecutorService)、Spring @Async 注解、CompletableFuture、@Scheduled + 异步、消息队列(MQ)、Spring WebFlux、以及 Java 21 虚拟线程。其中,Spring @Async 是 Spring Boot 项目中最推荐、最简洁的异步方案,难度仅一颗星,推荐度却高达五颗星。
本文将以天翼云的实战经验为背景,从原理到代码,从坑点到调优,把 @Async 拆个底朝天。
二、@Async 的底层原理:AOP 代理 + 线程池,就这么简单
@Async 的本质,是 Spring AOP 动态代理 + TaskExecutor 任务执行器的组合拳。
当你在方法上加了 @Async,Spring 启动时会通过 AsyncAnnotationBeanPostProcessor 扫描所有带 @Async 注解的 Bean,然后为它们生成代理对象。调用时,实际执行的是代理逻辑,而非原方法。代理做了什么?三步:
- 将方法封装成 Runnable/Callable 任务
- 提交给配置好的 TaskExecutor 线程池
- 在新线程中执行目标方法
代理方式分两种:目标类实现了接口 → JDK 动态代理;没实现接口 → CGLIB 生成子类字节码。
启用这一切的开关是 @EnableAsync。这个注解通过 @Import(AsyncConfigurationSelector.class) 导入了 ProxyAsyncConfiguration,它负责注册 AsyncAnnotationBeanPostProcessor 并配置默认的任务执行器。
核心流程图:
调用 @Async 方法
↓
被 AOP 代理拦截
↓
封装为 Runnable / Callable
↓
提交给 TaskExecutor 线程池
↓
异步线程中执行目标方法
三、完整代码实战:从零搭建一个生产级异步方案
第一步:启用异步支持
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
没有 @EnableAsync,@Async 注解就是摆设,所有方法都会同步执行。
第二步:自定义线程池(强烈推荐)
Spring 默认的 SimpleAsyncTaskExecutor 不是真正的线程池——它来一个任务就创建一个新线程,没有复用,高并发下直接 OOM。在天翼云的生产环境中,我们从不使用默认执行器,全部自定义:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数:一直存活的线程
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(100); // 任务队列容量
executor.setThreadNamePrefix("async-"); // 线程名前缀,方便排查
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, params) -> {
System.err.println("异步方法异常: " + method.getName() + ", 原因: " + throwable.getMessage());
};
}
}
线程池六大参数调优要点:
| 参数 | 含义 | 天翼云推荐值 |
|---|---|---|
| corePoolSize | 核心线程数,空闲也不销毁 | 根据平均负载设,如 5 |
| maxPoolSize | 最大线程数 | core 的 1.5~2 倍 |
| queueCapacity | 任务队列容量 | 100 左右,太大易 OOM |
| keepAliveTime | 空闲线程存活时间 | 60 秒 |
| threadNamePrefix | 线程名前缀 | 必设,方便日志定位 |
| rejectedExecutionHandler | 拒绝策略 | CallerRunsPolicy(调用者自己跑) |
第三步:编写异步方法
@Service
public class OrderService {
@Async("taskExecutor")
public void sendSmsNotify(String phone) {
try {
Thread.sleep(2000); // 模拟发送短信
System.out.println("短信已发送至: " + phone + ", 线程: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Async("taskExecutor")
public CompletableFuture<String> processOrder(Long orderId) {
// 模拟耗时业务
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return CompletableFuture.completedFuture("订单" + orderId + "处理完成");
}
}
第四步:Controller 调用
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/create")
public String createOrder() {
orderService.sendSmsNotify("13800138000"); // 异步,不阻塞
return "订单创建成功";
}
@GetMapping("/process")
public String processOrder() throws Exception {
CompletableFuture<String> future = orderService.processOrder(10001L);
String result = future.get(); // 阻塞等待结果,生产中建议用回调
return result;
}
}
四、@Async 的五大失效场景(踩坑实录)
在天翼云的项目复盘中,@Async 失效的案例占了异步问题的 80%。以下五个坑,务必避开:
| 失效场景 | 原因 | 解决方案 |
|---|---|---|
| 同类内部调用(this.asyncMethod()) | Spring AOP 只能拦截外部调用,内部调用绕过了代理 | 把异步方法抽到另一个 Service 类 |
| 非 public 方法 | AOP 只能拦截 public 方法 | 改为 public |
| static / final / private 方法 | 代理机制无法处理 | 去掉这些修饰符 |
| 未加 @EnableAsync | 异步功能未开启 | 启动类加 @EnableAsync |
| 返回值不支持 | 返回值必须是 void / Future / CompletableFuture | 修改返回类型 |
最经典的坑:同类调用失效。
@Service
public class BadService {
@Async
public void asyncMethod() {
System.out.println("异步执行");
}
public void callSelf() {
this.asyncMethod(); // ❌ 失效!走的是原始对象,不是代理
}
}
解决方案:把 asyncMethod 移到另一个 GoodService,由 BadService 注入调用。
五、@Async vs CompletableFuture:到底选哪个?
搜索 java async 的开发者,经常在这两个方案之间纠结。直接给结论:
| 维度 | @Async | CompletableFuture |
|---|---|---|
| 难度 | ⭐ 一颗星 | ⭐⭐ 两颗星 |
| 粒度 | 方法级 | 代码级,更细 |
| 任务组合 | 不支持 | 支持链式调用、并行组合 |
| 侵入性 | 极低,加个注解 | 中等,需要改写代码 |
| 适用场景 | 简单异步任务 | 复杂异步编排 |
实战建议: 简单场景用 @Async,复杂场景用 CompletableFuture,两者可以混用。比如 @Async 方法内部返回 CompletableFuture,既享受了声明式的简洁,又获得了链式调用的能力。
六、天翼云生产环境的最佳实践
在天翼云的 IoT 设备监控和订单处理系统中,我们总结出了一套异步调用的铁律:
- 必须自定义线程池,禁止使用默认的 SimpleAsyncTaskExecutor
- 核心线程数 = 平均并发量,最大线程数 = 核心 × 2,队列设 100
- 所有 @Async 方法必须在独立类中,杜绝 self-invocation
- 异常必须统一处理,通过 AsyncUncaughtExceptionHandler 捕获,否则异常直接吞掉,排查时毫无线索
- 返回值优先用 CompletableFuture,而非 void,方便调用方获取结果或链式处理
- 不要滥用异步——能同步且耗时 < 50ms 的操作,别异步,上下文切换本身也有成本