Java 异步编程
什么是异步?为什么工作中必须用?
- 同步 VS 异步
同步:你去吃饭,必须等饭做好、端上来,你才能做别的事(阻塞等待)。
异步:你点餐后,去玩手机、聊天,饭好了服务员通知你(不阻塞,后台执行)。
- 工作中为什么要用异步?(核心价值)
1)提升接口响应速度:比如用户注册 → 发短信 / 邮件 → 记录日志,同步要等所有步骤完成,异步只需要执行核心注册逻辑。
2)提高系统吞吐量:不阻塞线程,能处理更多请求。
3)解耦业务逻辑:核心流程和非核心流程分离。
4)避免线程阻塞:比如调用第三方接口、IO 操作(文件、数据库),非常适合异步。
一、Java异步编程的4种核心实现方式
从简单到高级,企业 99% 场景都用这 4 种:
1、基础:Thread + 匿名内部类
2、进阶:Runnable + 线程池
3、标准:Future(JDK 1.5)
4、推荐:CompletableFuture(JDK 1.8+,工作主力)
二、异步编程基础 —— Thread + Runnable
2.1 核心概念
Runnable:无返回值、无异常抛出的异步任务接口。
Thread:JVM 线程,直接启动异步执行。
2.2 代码实现(最基础代码)
public class AsyncDemo {
public static void main(String[] args) {
System.out.println("主线程开始:" + Thread.currentThread().getName());
// 1. 创建异步任务
Runnable task = () -> {
System.out.println("异步任务执行:" + Thread.currentThread().getName());
// 模拟业务:发短信、写日志、调用第三方接口
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步任务执行完成");
};
// 2. 启动异步线程
new Thread(task).start();
System.out.println("主线程结束,不用等待异步任务");
}
}
2.3 关键细节
1、start() 和 run() 区别
start():真正启动新线程,异步执行。
run():只是普通方法调用,同步执行,不是异步!
2、缺点:
无返回值
无法处理异常
频繁创建线程耗资源,工作中绝对禁止直接用 new Thread
三、工作基础 —— 线程池 + Runnable
工作中严禁手动创建线程,必须用线程池!
3.1 为什么要用线程池?
1、复用线程,减少创建 / 销毁开销
2、控制并发数量,避免服务器崩溃
3、统一管理线程生命周期
3.2 JDK 提供的线程池(工作常用)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolAsyncDemo {
public static void main(String[] args) {
// 1. 创建线程池(工作推荐自定义,不推荐 Executors)
ExecutorService threadPool = Executors.newFixedThreadPool(5);
System.out.println("主线程:" + Thread.currentThread().getName());
// 2. 提交异步任务
threadPool.execute(() -> {
System.out.println("线程池异步任务:" + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("异步任务完成");
});
System.out.println("主线程继续执行");
// 3. 关闭线程池(项目中是全局单例,不用关闭)
threadPool.shutdown();
}
}
3.3 关键细节
1、execute() vs submit()
-
execute():无返回值,只能执行 Runnable
-
submit():有返回值 Future,可获取结果
2、工作禁止用 Executors 快捷创建线程池!
-
newFixedThreadPool:队列无限长,OOM
-
newCachedThreadPool:线程无限创建,OOM
3、工作标准写法:自定义线程池
import java.util.concurrent.*;
public class CustomThreadPool {
// 全局单例线程池(Spring 项目可交给容器管理)
public static final ExecutorService ASYNC_POOL = new ThreadPoolExecutor(
5, // 核心线程
10, // 最大线程
60L, // 空闲时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列(关键!)
new ThreadFactory() { // 自定义线程名(排错神器)
final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
@Override
public Thread newThread(Runnable r) {
Thread thread = defaultFactory.newThread(r);
thread.setName("async-work-thread-" + thread.getId());
return thread;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
}
拒绝策略说明:
-
CallerRunsPolicy:主线程执行(最安全,不丢任务)
-
AbortPolicy:抛异常(默认)
-
DiscardPolicy:丢弃任务
-
DiscardOldestPolicy:丢弃最老任务
四、带返回值的异步 —— Future
4.1 核心作用
解决 Runnable 无返回值的问题,可以获取异步任务执行结果。
4.2 代码实战演示
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class FutureDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = CustomThreadPool.ASYNC_POOL;
System.out.println("主线程开始");
// 1. 提交有返回值的任务
Future<String> future = pool.submit(() -> {
System.out.println("异步任务执行");
Thread.sleep(1000);
return "异步任务执行结果:success";
});
System.out.println("主线程做其他事...");
// 2. 阻塞获取结果(注意:会阻塞!)
String result = future.get();
System.out.println("获取异步结果:" + result);
pool.shutdown();
}
}
4.3 Future关键细节
1、get() 会阻塞主线程
调用 get() 时,如果任务没执行完,主线程会一直等 → 变成伪异步!
2、get(timeout, unit) 必须加超时时间
// 最多等 500ms,超时抛异常
future.get(500, TimeUnit.MILLISECONDS);
3、无法监听任务完成
4、不支持链式调用、多任务组合
所以:Future 工作中很少直接用,只做过渡了解。
五、工作主力 —— CompletableFuture(终极方案)
JDK 1.8 推出,真正的异步编程神器,解决 Future 所有痛点:
-
支持回调,不阻塞
-
支持链式调用
-
支持多任务组合(串行、并行、汇聚)
-
支持异常处理
-
支持自定义线程池
5.1 创建 CompletableFuture 的 4 种方法
// 1. 无返回值,使用默认线程池
CompletableFuture.runAsync(() -> {});
// 2. 有返回值,使用默认线程池
CompletableFuture.supplyAsync(() -> "返回值");
// 3. 无返回值 + 自定义线程池(工作推荐!)
CompletableFuture.runAsync(() -> {}, 自定义线程池);
// 4. 有返回值 + 自定义线程池(工作最常用!)
CompletableFuture.supplyAsync(() -> "结果", 自定义线程池);
5.2 核心API细节讲解(工作高频)
5.2.1 任务完成回调:thenAccept /thenApply
-
thenAccept:接收上一步结果,无返回值
-
thenApply:接收上一步结果,有返回值
CompletableFuture.supplyAsync(() -> {
return "用户ID:1001";
}, CustomThreadPool.ASYNC_POOL)
.thenApply(result -> {
// 处理结果,有返回值
return result + " → 发送短信";
})
.thenAccept(result -> {
// 处理结果,无返回值
System.out.println("最终结果:" + result);
});
5.2.2 异步继续执行:
thenApplyAsync /thenAcceptAsync
加 Async 表示下一步继续用线程池线程执行,不占用主线程。
5.2.3 任务完成后处理:whenComplete
无论成功 / 异常,都会执行:
.whenComplete((result, ex) -> {
if(ex != null) {
System.out.println("异常:" + ex.getMessage());
} else {
System.out.println("成功:" + result);
}
});
5.2.4 异常捕获:exceptionally
相当于 try-catch:
.exceptionally(ex -> {
System.out.println("异步任务异常:" + ex.getMessage());
return "默认降级结果";
});
5.2.5 工作最强:多任务组合
① 串行执行(A → B → C)
CompletableFuture
.supplyAsync(任务A)
.thenApplyAsync(任务B)
.thenApplyAsync(任务C);
② 并行执行 + 等待全部完成(allOf)
CompletableFuture<Void> allTask = CompletableFuture.allOf(
CompletableFuture.runAsync(任务1),
CompletableFuture.runAsync(任务2),
CompletableFuture.runAsync(任务3)
);
// 等待所有任务完成
allTask.join();
③ 并行执行 + 任意一个完成就返回(anyOf)
CompletableFuture<Object> anyTask = CompletableFuture.anyOf(任务1,任务2);
Object result = anyTask.join();
5.3 工作实战:用户注册异步优化(真实业务)
需求
用户注册接口:
-
保存用户信息(核心,必须同步)
-
发送短信(异步)
-
发送邮件(异步)
-
记录日志(异步)
-
赠送优惠券(异步)
同步执行:总耗时 = 1+1+1+1 = 4 秒
异步执行:总耗时 = 1 秒
实战代码:
@Service
public class UserService {
@Resource
private UserMapper userMapper;
// 注入我们自定义的全局线程池
@Resource
private ExecutorService asyncPool;
public String register(User user) {
long start = System.currentTimeMillis();
System.out.println("注册开始,线程:" + Thread.currentThread().getName());
// 1. 核心逻辑:保存用户(同步)
userMapper.insert(user);
// 2. 异步执行非核心任务
CompletableFuture<Void> smsTask = CompletableFuture.runAsync(() -> {
sendSms(user.getPhone());
}, asyncPool);
CompletableFuture<Void> emailTask = CompletableFuture.runAsync(() -> {
sendEmail(user.getEmail());
}, asyncPool);
CompletableFuture<Void> logTask = CompletableFuture.runAsync(() -> {
saveLog(user.getId());
}, asyncPool);
CompletableFuture<Void> couponTask = CompletableFuture.runAsync(() -> {
giveCoupon(user.getId());
}, asyncPool);
// 3. 组合所有异步任务(可选等待)
CompletableFuture.allOf(smsTask, emailTask, logTask, couponTask).join();
System.out.println("注册完成,耗时:" + (System.currentTimeMillis() - start) + "ms");
return "注册成功";
}
// 发短信
private void sendSms(String phone) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("短信发送成功:" + phone);
}
// 发邮件
private void sendEmail(String email) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("邮件发送成功:" + email);
}
// 保存日志
private void saveLog(Long userId) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("日志保存成功:" + userId);
}
// 赠送优惠券
private void giveCoupon(Long userId) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("优惠券赠送成功:" + userId);
}
}
六、Spring 中异步 —— @Async 注解
工作中 SpringBoot 项目最简单的异步用法,底层就是线程池 + AOP。
6.1 使用步骤
1、启动类开启异步
@SpringBootApplication
@EnableAsync // 开启异步
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2、方法上加 @Async
@Service
public class AsyncService {
// 异步方法
@Async("asyncPool") // 指定线程池名称
public void sendSms(String phone) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("异步发短信:" + phone);
}
}
3、配置自定义线程池
@Configuration
public class ThreadPoolConfig {
@Bean("asyncPool")
public Executor asyncPool() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new CustomThreadFactory("async"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
return executor;
}
}
6.2 @Async 关键坑点(必看,否则异步失效)
-
必须用 Spring 注入的对象调用,不能内部 this.方法()
-
方法不能是 private /static/final
-
必须加 @EnableAsync
-
避免事务失效:异步方法和事务方法不要混用
七、异步编程必须注意的坑(工作保命)
7.1 异步线程上下文丢失
主线程的 RequestAttributes、ThreadLocal 无法传递到异步线程
解决方案:手动传递上下文
// 手动传递 RequestContext
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(attributes);
// 执行业务
}, asyncPool);
7.2 异步线程异常吞没
-
异步线程抛出异常,主线程感知不到
-
必须用 exceptionally / whenComplete 捕获
7.3 线程池参数不合理导致问题
-
核心线程太小:任务堆积
-
最大线程太大:CPU 飙升
-
队列无界:OOM 内存溢出
7.4 主线程结束,异步任务中断
比如测试方法 main 函数,主线程结束,异步线程会被杀死
解决方案:join() / get() 等待,或使用守护线程
7.5 异步导致数据不一致
-
比如:用户注册成功,短信发送失败
-
解决方案:重试机制 + 消息队列(RabbitMQ/RocketMQ) 保证最终一致性
八、异步编程工作最佳实践
1、所有异步必须用自定义线程池,禁止用 Executors 和 new Thread ()
2、线程必须自定义名称,方便排查问题
3、异步方法必须捕获异常,记录日志
4、耗时 IO 操作(HTTP、DB、MQ、文件)全部异步化
5、核心流程同步,非核心流程异步
6、CompletableFuture 优先于 @Async,更灵活
7、高并发场景用消息队列替代异步,保证可靠性
整体总结:
基础异步:Thread + Runnable → 工作禁用
标准异步:线程池 + Future → 有返回值但会阻塞
主力异步:CompletableFuture → 工作 90% 场景首选
简化异步:@Async → Spring 项目快速实现
核心原则:自定义线程池、不阻塞、捕获异常、上下文安全
最终目标:提升接口响应速度、提高系统吞吐量、解耦业务
浙公网安备 33010602011771号