⚡ 异步编程与多线程实战

CompletableFuture · @Async · 多线程问题 · 异步踩坑 · 生产改造 · 10章全链路

CompletableFuture@Async线程池 竞态条件死锁异常被吞 上下文丢失ThreadLocal生产场景
学习路径1. 理解概念类比+图解2. 看代码IDE窗口+实战3. 动手踩坑+FAQ
三步学习法:概念→代码→实战
📦 P1: 异步编程全景 — 4种方式对比
你写代码的时候,有没有遇到过这种情况:一个接口要调3个服务,每个200ms,串行跑下来要600ms,用户等得花儿都谢了。

其实你心里也清楚——这3个调用互不依赖,完全可以同时跑。但异步怎么写?CompletableFuture?@Async?还是直接new Thread?

这篇就从零讲清楚:异步任务怎么写、多线程有什么坑、异步任务有什么坑。
类比:同步 vs 异步
  · 同步 = 你亲自跑腿送文件,送到A楼再去B楼,一趟一趟跑
  · 异步 = 你交代给3个同事,让他们同时出发,你坐等结果
  · 多线程的问题 = 3个同事同时改同一份Excel,后保存的把前面的覆盖了
🔄 为什么需要异步?
同步调用(串行)
查库存 200ms
↓ 等完才下一步
查优惠券 200ms
↓ 等完才下一步
查地址 200ms

总耗时:600ms
异步调用(并行)
查库存 200ms ┐
查优惠券 200ms ├ 同时跑!
查地址 200ms ┘
↓ 全部完成才继续
总耗时:200ms
📊 4种异步方式对比
方式类比代码量返回值异常处理适用场景
Thread + Runnable 你自己骑自行车送文件 ❌ 无 ❌ 手动try-catch 几乎不用了
ExecutorService 公司有专门跑腿部门,
你提交任务就行
✅ Future ⚠️ 藏在Future里 简单异步任务
不需要链式编排
CompletableFuture 跑腿部门+自动流转
(送完A自动送B)
✅ 任意类型 ✅ exceptionally/handle ★★★ 最推荐
链式/并行/编排
Spring @Async 在群里@同事帮忙
(最简单)
最少 ⚠️ Future类型受限 ❌ 容易被吞 简单的"开个线程跑"
不需要复杂编排
📝 4种方式的代码对比
4种方式代码对比 — AsyncDemo.java
① Thread + Runnable(最原始)
1
new Thread(() -> {
2
System.out.println("异步任务执行中...");
3
}).start();
4
// ❌ 没有返回值、没有异常处理、每次都创建新线程(浪费资源)
② ExecutorService(线程池)
1
ExecutorService pool = Executors.newFixedThreadPool(4);
2
Future<String> future = pool.submit(() -> {
3
return "查询结果:库存充足";
4
});
5
String result = future.get(); // ⚠️ 阻塞等待,还可能藏着异常
③ CompletableFuture(★推荐)
1
CompletableFuture<String> cf = CompletableFuture
2
.supplyAsync(() -> "查询结果:库存充足")
3
.thenApply(result -> result + ",可以下单")
4
.exceptionally(ex -> "查询失败:" + ex.getMessage());
5
// ✅ 有返回值、有链式编排、有异常处理、自动用线程池
④ Spring @Async(最简单)
1
@Async
2
public CompletableFuture<String> checkStock() {
3
return CompletableFuture.completedFuture("库存充足");
4
}
5
// ✅ 最简单 ⚠️ 但有很多坑(后面P4专门讲)
P1 总结
① 能用 CompletableFuture 就用它——链式编排、异常处理、并行组合都最强
② @Async 看起来最简单,但坑最多(P4专门讲)
③ Thread/Runnable 基本不用了,ExecutorService 看场景
④ 选型口诀:简单异步→@Async,复杂编排→CompletableFuture
🔗 P2: CompletableFuture 深度实战
类比:CompletableFuture 就像外卖配送链——
  · 你下单(supplyAsync)→ 商家接单做餐(thenApply)→ 打包(thenApply)→ 骑手配送(thenAccept)
  · 每一步自动流转到下一步,你不用每步都等着
  · 做餐失败(exceptionally)→ 你收到退款通知
  · 不像以前:你站在店门口等做餐→等打包→等骑手,一步步傻等
📦 4种创建方式
// 1. supplyAsync — 有返回值(最常用) CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> { return "查询库存:充足"; }); // 2. runAsync — 无返回值(像 void 方法) CompletableFuture<Void> cf2 = CompletableFuture.runAsync(() -> { System.out.println("发送邮件通知..."); }); // 3. completedFuture — 直接包装一个已有值 CompletableFuture<String> cf3 = CompletableFuture.completedFuture("已有数据"); // 4. supplyAsync + 自定义线程池(生产推荐!) ExecutorService bizPool = Executors.newFixedThreadPool(8); CompletableFuture<String> cf4 = CompletableFuture.supplyAsync(() -> { return "查询库存:充足"; }, bizPool); // ← 指定自己的线程池,不用默认的 ForkJoinPool
⚠️ 为什么生产环境一定要传自定义线程池?
默认的 ForkJoinPool.commonPool()全局共享的——你所有 CompletableFuture、并行流都在抢同一个池子。一个慢任务就能拖垮所有异步操作!后面P9详细讲。
🔗 链式调用 — 外卖配送链
外卖配送链 vs CompletableFuture链:
下单
supplyAsync
商家做餐
thenApply
打包
thenCompose
配送
thenAccept
如果做餐失败 → 退款通知
exceptionally
// 完整示例:模拟电商下单前的数据查询链 CompletableFuture<String> result = CompletableFuture // 第1步:查库存(supplyAsync = 有返回值的异步任务) .supplyAsync(() -> { sleep(200); // 模拟耗时200ms return "库存=100"; }) // 第2步:拿到上一步的结果,继续处理(thenApply = 同步转换) .thenApply(stock -> { System.out.println("收到: " + stock); // 收到: 库存=100 return stock + ", 可以下单"; }) // 第3步:最终消费结果(thenAccept = 只消费,不返回) .thenAccept(finalResult -> { System.out.println("最终结果: " + finalResult); // 输出: 最终结果: 库存=100, 可以下单 }) // 第4步:异常处理(exceptionally = 出错了怎么办) .exceptionally(ex -> { System.err.println("出错了: " + ex.getMessage()); return null; }); result.join(); // 等待整个链完成
$ 运行结果: 收到: 库存=100 最终结果: 库存=100, 可以下单
🤔 thenApply vs thenCompose — 同一部门流转 vs 跨部门流转
类比
  · thenApply = 同一部门流转。你在市场部,同事把文件给你,你盖个章继续传给下一个同事(返回值直接是结果)
  · thenCompose = 跨部门流转。你在市场部,但下一步需要财务部审批,你得等财务部走完他们的流程(返回值是另一个CompletableFuture,自动"展平")
// thenApply:同步转换(像 map) CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "库存=100") .thenApply(stock -> stock + ", 可以下单"); // 返回类型:CompletableFuture<String> // thenCompose:异步流转(像 flatMap) CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> "库存=100") .thenCompose(stock -> // ← 注意这里返回的是 CompletableFuture,不是 String checkCoupon(stock) // 这个方法返回 CompletableFuture<String> ); // thenCompose 自动"展平",不会变成 CompletableFuture<CompletableFuture<String>> // 对比:如果 thenApply 返回 CompletableFuture 会怎样? CompletableFuture<CompletableFuture<String>> nested = // ❌ 嵌套了! CompletableFuture.supplyAsync(() -> "库存=100") .thenApply(stock -> checkCoupon(stock)); // 变成了 CompletableFuture<CompletableFuture<String>> 嵌套!
🛡️ 异常处理3种方式
// ① exceptionally — 出错了给个默认值(像 catch + 返回默认值) CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> { if (true) throw new RuntimeException("库存查询失败"); return "库存=100"; }).exceptionally(ex -> { System.out.println("异常: " + ex.getMessage()); return "库存查询失败,请稍后重试"; // 降级返回 }); System.out.println(cf1.join()); // 输出: 异常: java.lang.RuntimeException: 库存查询失败 // 输出: 库存查询失败,请稍后重试 // ② handle — 无论成功失败都走这里(像 finally + 可以改结果) CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> "库存=100") .handle((result, ex) -> { if (ex != null) { return "出错了: " + ex.getMessage(); } return result + ", 处理完成"; }); System.out.println(cf2.join()); // 输出: 库存=100, 处理完成 // ③ whenComplete — 只记录,不改结果(像 finally + 不改返回值) CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(() -> "库存=100") .whenComplete((result, ex) -> { if (ex != null) { System.out.println("记录日志: " + ex.getMessage()); } else { System.out.println("记录日志: 成功,结果=" + result); } // 注意:这里不能改返回值!只能做记录 }); System.out.println(cf3.join()); // 输出: 记录日志: 成功,结果=库存=100 // 输出: 库存=100
方法类比能否改结果是否消费异常典型用途
exceptionallycatch + 默认值✅ 能✅ 是降级返回
handlecatch + finally✅ 能✅ 是成功/失败统一处理
whenCompletefinally(不改值)❌ 不能⚠️ 不消费记录日志/监控
🎯 P3: CompletableFuture 组合与编排
🔄 三种编排方式
thenCombine — 两个都完成,合并
类比:等外卖+等快递都到了才开吃

场景:查库存 + 查优惠券,
两个结果合并判断能否下单
allOf — 等所有完成
类比:等6个同事都到齐才开会

场景:查库存+查优惠券+查地址,
全部查完才继续
anyOf — 任一完成即可
类比:抢票,谁先抢到算谁的

场景:调3个供应商接口,
谁先返回就用谁的
🛒 实战:电商下单 — 3个查询并行
电商下单前的并行查询时序:
主线程 ─────┬── 启动3个异步查询 ──────────────────── 汇总结果 ─── 下单 │ │ 查库存 ─────┼──── 200ms ──── 库存=100 ──────────┤ 查优惠券 ─────┼──── 150ms ──── 优惠券=满100减20 ────┤ 查地址 ─────┼──── 100ms ──── 地址=北京市 ──────────┘ │ │ │ max(200,150,100) = 200ms │ │ (取最慢的那个,而不是加起来 450ms) │
// 电商下单:3个查询并行 → 汇总 → 下单 public OrderResult createOrder(String userId, String productId) { // 3个查询同时发起(并行) CompletableFuture<String> stockFuture = CompletableFuture.supplyAsync(() -> checkStock(productId), bizPool); // 200ms CompletableFuture<String> couponFuture = CompletableFuture.supplyAsync(() -> checkCoupon(userId), bizPool); // 150ms CompletableFuture<String> addressFuture = CompletableFuture.supplyAsync(() -> checkAddress(userId), bizPool); // 100ms // 等待全部完成,汇总结果 CompletableFuture<OrderResult> orderFuture = CompletableFuture .allOf(stockFuture, couponFuture, addressFuture) .thenApply(v -> { // allOf 完成后,3个 Future 都已经有结果了 String stock = stockFuture.join(); // 不阻塞,立即返回 String coupon = couponFuture.join(); // 不阻塞,立即返回 String address = addressFuture.join(); // 不阻塞,立即返回 return new OrderResult(stock, coupon, address); }); return orderFuture.join(); // 等待最终结果 } // 输出结果: // OrderResult{stock='库存=100', coupon='满100减20', address='北京市朝阳区xx号'} // 总耗时:约200ms(取最慢的),而不是450ms(串行累加)
⏰ 超时处理
// orTimeout — 超时自动失败(Java 9+) CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> { sleep(5000); // 模拟慢查询5秒 return "结果"; }).orTimeout(2, TimeUnit.SECONDS); // 2秒超时 // 如果2秒内没完成 → 自动抛出 TimeoutException try { cf.join(); } catch (CompletionException e) { System.out.println("超时了: " + e.getCause()); // 输出: 超时了: java.util.concurrent.TimeoutException } // completeOnTimeout — 超时给默认值(更友好) CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> { sleep(5000); return "结果"; }).completeOnTimeout("查询超时,请稍后重试", 2, TimeUnit.SECONDS); System.out.println(cf2.join()); // 输出: 查询超时,请稍后重试
P3 总结
thenCombine:两个并行 → 合并结果
allOf:多个并行 → 全部完成 → 汇总(最常用!)
anyOf:多个并行 → 任一完成即可(抢票/多供应商场景)
orTimeout / completeOnTimeout:防止异步任务无限等待
allOf 后要用 join() 取各 Future 的值,不要用 get()(get 受检异常太烦)
⚠️ P4: Spring @Async 实战与5大踩坑
类比:@Async 就像在群里@同事帮忙——你发条消息就走了,同事自己去做。最简单,但如果不了解规则,坑很多。
✅ 基本用法(3步搞定)
// 第1步:启动类上加 @EnableAsync @SpringBootApplication @EnableAsync public class Application { ... } // 第2步:方法上加 @Async @Service public class EmailService { @Async public void sendEmail(String to, String content) { sleep(1000); // 模拟发邮件耗时 System.out.println("邮件已发送给: " + to); } } // 第3步:调用(像普通方法一样调,但实际是异步的) @RestController public class OrderController { @Autowired private EmailService emailService; @PostMapping("/order") public String createOrder() { emailService.sendEmail("user@test.com", "下单成功"); return "下单成功"; // 立即返回,不用等邮件发完 } }
🚨 踩坑1:@Async 不生效!——同类内部调用
这是最常见的坑!在同一个类里调 @Async 方法,异步不生效。因为 Spring AOP 是基于代理的,内部调用不走代理。
@Service public class OrderService { // ❌ 错误:同类内部调用,@Async 不生效! public String createOrder() { sendEmail(); // ← 这是 this.sendEmail(),不走代理!同步执行! return "下单成功"; } @Async public void sendEmail() { System.out.println("发送邮件..."); } } // ✅ 正确:把 @Async 方法放到另一个 Service 里 @Service public class EmailService { @Async public void sendEmail() { System.out.println("发送邮件..."); } } @Service public class OrderService { @Autowired private EmailService emailService; public String createOrder() { emailService.sendEmail(); // ✅ 通过代理调用,异步生效! return "下单成功"; } }
🚨 踩坑2:@Async 异常被吞!
@Service public class ReportService { @Async public void generateReport() { int i = 1 / 0; // ← 抛出 ArithmeticException System.out.println("这行不会执行"); } } // 调用方: reportService.generateReport(); System.out.println("调用完成"); // 正常打印,好像什么都没发生! // ❌ 异常完全被吞了!没有任何日志!你都不知道出错了! // 类比:寄快递没留电话,出了问题通知不到你
修复:2种方式
① 方法返回 CompletableFuture,异常可以被 exceptionally 捕获
② 配置自定义的 AsyncUncaughtExceptionHandler
// 修复方式1:返回 CompletableFuture @Async public CompletableFuture<String> generateReport() { try { int i = 1 / 0; return CompletableFuture.completedFuture("成功"); } catch (Exception e) { return CompletableFuture.failedFuture(e); // ← 异常包装进 Future } } // 修复方式2:全局异常处理器 @Configuration public class AsyncConfig implements AsyncConfigurer { @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex, method, params) -> { System.err.println("异步任务异常! 方法=" + method.getName() + ", 参数=" + Arrays.toString(params) + ", 异常=" + ex.getMessage()); }; } } // 输出: 异步任务异常! 方法=generateReport, 参数=[], 异常=/ by zero
🚨 踩坑3:@Async 默认线程池无界
类比:公司请了无限多的实习生,来多少活儿就招多少——听起来美好,但人一多就占满了整栋楼(内存溢出OOM)。
// @Async 不指定线程池时,默认用 SimpleAsyncTaskExecutor // 这个执行器:每来一个任务就创建一个新线程!没有上限! // 高并发下 → 线程数爆增 → OOM // ✅ 正确:配置自定义线程池 @Configuration @EnableAsync public class AsyncConfig { @Bean("bizAsyncPool") public Executor bizAsyncPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); // 核心线程数 executor.setMaxPoolSize(20); // 最大线程数 executor.setQueueCapacity(100); // 队列容量 executor.setThreadNamePrefix("biz-async-"); // 线程名前缀(方便排查) executor.setRejectedExecutionHandler(new CallerRunsPolicy()); // 拒绝策略 executor.initialize(); return executor; } } // 使用时指定线程池 @Async("bizAsyncPool") public void sendEmail() { ... }
🚨 踩坑4:@Async + @Transactional 事务失效
@Service public class OrderService { @Async @Transactional // ⚠️ 事务在异步线程中! public void processOrder(Long orderId) { // 这个方法在异步线程中执行 // Spring 事务是基于 ThreadLocal 的 // 异步线程 ≠ 主线程 → 主线程的事务上下文丢了! // // 情况1:异常回滚 — 只回滚异步线程的事务,主线程不知道 // 情况2:主线程已经提交了,异步线程还在跑 → 数据不一致 orderMapper.updateStatus(orderId, "PROCESSING"); } } // ✅ 正确做法:事务放在主线程,异步只做不需要事务的操作 @Transactional public void createOrder() { orderMapper.insert(order); // 主线程事务内操作 emailService.sendEmail(); // 异步发邮件(不需要事务) }
🚨 踩坑5:返回值只有 void / Future / CompletableFuture
@Async public String getData() { // ❌ 返回 String,调用方拿到的永远是 null! return "数据"; } // 调用方: String data = service.getData(); // data = null!因为实际在异步线程执行 // ✅ 正确:返回 CompletableFuture @Async public CompletableFuture<String> getData() { return CompletableFuture.completedFuture("数据"); } // 调用方: CompletableFuture<String> future = service.getData(); String data = future.join(); // ✅ 拿到 "数据"
@Async 5大坑总结
① 同类内部调用 → 不走代理 → @Async不生效
② 异常被吞 → 毫无感知 → 必须配 AsyncUncaughtExceptionHandler
③ 默认线程池无界 → OOM风险 → 必须自定义线程池
④ @Async + @Transactional → 事务上下文丢失 → 事务放主线程
⑤ 返回值不是 Future/CompletableFuture → 调用方拿 null
💥 P5: 多线程问题 — 竞态条件(Race Condition)
类比:两个人同时改同一个Excel单元格——
  · A 打开文件,看到余额=1000
  · B 也打开文件,看到余额=1000
  · A 取500,写入余额=500,保存
  · B 取200,写入余额=800,保存
  · 最终余额=800(B覆盖了A的结果!)
  · 正确应该是余额=300,但丢了A的那次操作
🧪 经典案例:不安全的 Counter
// 看起来没问题?其实问题很大! public class UnsafeCounter { private int count = 0; public void increment() { count++; // ← 这一行不是原子操作! // 编译后实际上是3步: // 1. 读取 count 的值(比如 0) // 2. 加 1(得到 1) // 3. 写回 count(写入 1) // 两个线程同时读到 0 → 都加1 → 都写1 → 丢了1次! } } // 测试:1000次 ++,结果应该=1000 UnsafeCounter counter = new UnsafeCounter(); IntStream.range(0, 1000).parallel().forEach(i -> counter.increment()); System.out.println("结果: " + counter.count); // 多次运行结果随机:973、988、991、967... // 几乎不可能得到 1000!
count++ 不是原子操作,两个线程可能交叉执行:
线程A ── 读count=0 ── 加1得1 ────────── 写1 ──→ count=1 线程B ──────────── 读count=0 ── 加1得1 ── 写1 ──→ count=1
结果:加了2次,但count只从0变成了1(丢了1次!)
🛡️ 4种解决方式
// 方式1:synchronized(最简单,但粒度粗) public synchronized void increment() { count++; } // 方式2:ReentrantLock(更灵活,可设超时) private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } // 方式3:AtomicInteger(★推荐,CAS无锁,性能最好) private final AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // ← CAS 无锁,极高并发下性能好 } // 方式4:LongAdder(超高并发下比 AtomicInteger 更快) private final LongAdder count = new LongAdder(); public void increment() { count.increment(); // ← 分段累加,超高并发下最快 }
方式性能灵活性适用场景
synchronized低(不能超时)简单场景、代码块短
ReentrantLock高(可超时/可中断)需要超时/公平锁
AtomicInteger计数器/累加器(★最常用)
LongAdder最高低(只能累加)统计计数(QPS/访问量)
竞态条件总结
① 本质:读→改→写不是原子操作,多线程交叉执行导致结果错误
② 识别:多个线程访问同一个共享变量且至少一个 → 就可能有竞态
③ 选型口诀:简单加锁synchronized,计数用Atomic,统计用LongAdder
🔒 P6: 多线程问题 — 死锁(Deadlock)
类比:两个人面对面走过道,都等对方让路——
  · 甲:"你先退回去,我再走"
  · 乙:"你先退回去,我再走"
  · 两个人都不退 → 谁也走不了 → 死锁
💀 经典案例:转账死锁
// 转账:A给B转100,同时B给A转50 public void transfer(Account from, Account to, int amount) { synchronized (from) { // 先锁转出账户 System.out.println("锁定: " + from.name); synchronized (to) { // 再锁转入账户 from.debit(amount); to.credit(amount); } } } // 线程1:A→B → 先锁A,再锁B // 线程2:B→A → 先锁B,再锁A // // 如果时序恰好: // 线程1 锁住A → 等锁B(被线程2占着) // 线程2 锁住B → 等锁A(被线程1占着) // → 互相等待 → 死锁!
死锁循环等待:
线程1:持有 锁A → 等待 锁B
线程2:持有 锁B → 等待 锁A
锁A ⇄ 互相等待 锁B
循环等待 → 永远无法解除 → 死锁
🔍 用 jstack 检测死锁
$ jps 12345 DeadLockDemo $ jstack 12345 ... Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x0000000xxxx (object 0x000000076..., a Account$A), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x0000000yyyy (object 0x000000076..., a Account$B), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at DeadLockDemo.transfer(DeadLockDemo.java:15) - waiting to lock <0x000000076...> (a Account$A) - locked <0x000000076...> (a Account$B) "Thread-0": at DeadLockDemo.transfer(DeadLockDemo.java:15) - waiting to lock <0x000000076...> (a Account$B) - locked <0x000000076...> (a Account$A)
🛡️ 预防死锁的5种策略
// 策略1:固定加锁顺序(最常用!) // 所有人都先锁A再锁B,就不会循环等待了 public void safeTransfer(Account from, Account to, int amount) { Account first = from.id < to.id ? from : to; // ← 按ID排序 Account second = from.id < to.id ? to : from; synchronized (first) { synchronized (second) { from.debit(amount); to.credit(amount); } } } // 策略2:超时放弃(tryLock with timeout) ReentrantLock lockA = new ReentrantLock(); ReentrantLock lockB = new ReentrantLock(); try { if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) { try { if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 成功拿到两把锁,执行业务 } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 策略3-5:减小锁粒度 / 使用无锁设计(Atomic/CAS) / 死锁检测工具(Arthas)
死锁总结
① 4个必要条件:互斥、持有并等待、不可抢占、循环等待(打破任一即可预防)
② 最实用的预防:固定加锁顺序(让所有人按同一顺序加锁)
③ 超时放弃:tryLock(timeout)lock() 安全得多
④ 检测工具:jstack / Arthas thread -b 查死锁
👁️ P7: 多线程问题 — 可见性与有序性
类比
  · 可见性问题 = 白板上写了通知,但有人没看白板还在用旧信息
  · 有序性问题 = 你说"先开门再开灯",但CPU觉得"先开灯再开门"也一样,就偷偷换了顺序
  · volatile = 用红色粗体写在白板上,保证所有人都能看到最新内容
👁️ 可见性问题:一个线程改了,另一个看不到
// ❌ 没有 volatile:子线程可能永远看不到 flag 的修改 public class VisibilityDemo { private static boolean running = true; // ❌ 没有 volatile public static void main(String[] args) throws Exception { new Thread(() -> { int count = 0; while (running) { // ← 子线程可能把 running 缓存在自己的 CPU 核心里 count++; // 主线程改成 false,但子线程看不到! } System.out.println("停止了,count=" + count); }).start(); Thread.sleep(1000); running = false; // 主线程改为 false // 但子线程可能永远不停!因为 running 的修改对子线程"不可见" } } // ✅ 加 volatile:保证所有线程都能看到最新值 private static volatile boolean running = true; // ✅ 加了 volatile
CPU缓存导致的可见性问题:
主线程 ── CPU1 缓存 ── running=false
子线程 ── CPU2 缓存 ── running=true(还是旧值!)
主存: 主内存 running=false(已经改了,但CPU2缓存没刷)
volatile = 强制读写都走主存 → 所有线程看到同一个值
🔀 有序性问题:指令重排
// 双重检查锁单例 — 为什么需要 volatile? public class Singleton { private static volatile Singleton instance; // ← 必须加 volatile! public static Singleton getInstance() { if (instance == null) { // 第1次检查 synchronized (Singleton.class) { if (instance == null) { // 第2次检查 instance = new Singleton(); // ← 这行可能被重排! } } } return instance; } } // new Singleton() 实际分3步: // 1. 分配内存空间 // 2. 调用构造方法初始化 // 3. 将引用指向内存地址 // // 重排后可能变成 1→3→2: // 线程A 执行了1→3(引用非null了,但对象还没初始化) // 线程B 第1次检查 instance!=null → 直接返回! // 但此时对象还没初始化完 → 用了半成品 → NPE! // // volatile 禁止重排 → 保证 1→2→3 顺序执行
可见性与有序性总结
① 可见性:一个线程改了变量,其他线程可能看不到(CPU缓存)
② 有序性:CPU和编译器可能偷偷换顺序(指令重排)
volatile 同时解决可见性和有序性
volatile 不保证原子性(count++ 还是需要锁/Atomic)
口诀:状态标志用volatile,计数用Atomic,复杂操作用锁
🚫 P8: 异步任务常见坑 — 异常被吞
类比:寄快递没留电话——
  · 你让同事去办事,但没告诉他"如果出问题给我打电话"
  · 结果他办砸了,默默收工回家了
  · 你还以为事情办好了 → 异常被吞了!
💀 场景1:CompletableFuture 的异常默认不打印
// ❌ 异常被静默吞掉 CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> { throw new RuntimeException("数据库连接失败!"); }); // 没有任何日志输出!异常就这么消失了! // 你都不知道出错了! // ✅ 修复:加上 exceptionally 或 handle CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> { throw new RuntimeException("数据库连接失败!"); }).exceptionally(ex -> { log.error("异步任务失败", ex); // ← 至少打印日志! return "默认值"; // 降级返回 });
💀 场景2:ExecutorService.submit() 的异常藏在 Future 里
ExecutorService pool = Executors.newFixedThreadPool(4); Future<String> future = pool.submit(() -> { throw new RuntimeException("出错了!"); }); // ❌ 没有任何输出!异常被 Future 对象"吞"了 // 异常藏在 future 里面,只有你调用 future.get() 时才会抛出 // 如果你从来不调 future.get() → 异常就永远消失了 // 这在生产环境很常见:异步任务出错了,但没人看 Future
💀 场景3:Thread 的 UncaughtExceptionHandler 没设置
// ❌ 直接 new Thread,异常直接消失 new Thread(() -> { throw new RuntimeException("线程异常!"); }).start(); // 控制台可能有输出(JVM默认打印到stderr),但你的监控系统收不到 // ✅ 修复:设置 UncaughtExceptionHandler Thread t = new Thread(() -> { throw new RuntimeException("线程异常!"); }); t.setUncaughtExceptionHandler((thread, ex) -> { log.error("线程[{}]异常", thread.getName(), ex); // 发送告警、记录到监控系统等 }); t.start();
🛡️ 统一异常处理最佳实践
// 1. CompletableFuture:永远链式调用 exceptionally 或 handle cf.exceptionally(ex -> { log.error(...); return defaultVal; }); // 2. @Async:配置 AsyncUncaughtExceptionHandler(见P4) // 3. 线程池:设置 ThreadFactory + UncaughtExceptionHandler ThreadPoolExecutor pool = new ThreadPoolExecutor(...); pool.setThreadFactory(r -> { Thread t = new Thread(r); t.setUncaughtExceptionHandler((thread, ex) -> { log.error("线程[{}]未捕获异常", thread.getName(), ex); }); return t; }); // 4. 全局兜底:CompletableFuture 框架方法 // 在每个 CompletableFuture 链的末尾,加上 whenComplete 做日志 cf.whenComplete((result, ex) -> { if (ex != null) log.error("异步任务最终异常", ex); });
异常被吞的4大场景
① CompletableFuture 不加 exceptionally → 异常消失
② @Async void 方法 → 异常被吞(见P4)
③ ExecutorService.submit() → 异常藏在 Future 里
④ Thread 没 UncaughtExceptionHandler → 异常不可控

一句话原则:异步任务的异常不会自动传播到调用方,你必须主动捕获
🔥 P9: 异步任务常见坑 — 线程池耗尽与上下文丢失
🏃 线程池耗尽 — 公司就5个工位,活儿来了没人接
类比:公司就5个工位(核心线程数=5),来了一堆活儿——
  · 前5个任务 → 5个工位全占满了
  · 第6-15个任务 → 放到待办区排队(队列容量=10)
  · 第16个任务 → 待办区也满了!→ 拒绝策略决定怎么办
  · CallerRunsPolicy → 让提交任务的人自己干(主线程阻塞!)
💀 场景1:ForkJoinPool.commonPool() — 所有CF共享一个池子
// ❌ 不指定线程池,所有 CompletableFuture 共享 ForkJoinPool.commonPool() // commonPool 的线程数 = CPU核数-1(比如8核机器只有7个线程) // 假设有两个业务都在用 CompletableFuture: // 业务A:导出报表(很慢,每个任务5秒) CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> { sleep(5000); // 慢任务,占住线程5秒 return "报表生成完成"; }); // ← 没传线程池,用的是 commonPool! // 业务B:查库存(本来很快,50ms) CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> { return "库存=100"; }); // ← 也没传线程池,也在用 commonPool! // 如果业务A同时跑了7个慢任务 → commonPool 全被占满! // 业务B的快任务只能在队列里等 → 50ms的查询等了5秒! // 看起来像是B很慢,实际是被A拖累的! // ✅ 修复:每个业务用自己的线程池 ExecutorService reportPool = Executors.newFixedThreadPool(4); // 报表专用 ExecutorService bizPool = Executors.newFixedThreadPool(8); // 业务查询专用 CompletableFuture.supplyAsync(slowTask, reportPool); // ← 报表用自己的池子 CompletableFuture.supplyAsync(fastTask, bizPool); // ← 查询用自己的池子
💀 场景2:CallerRunsPolicy 导致主线程阻塞
// 线程池配置:核心8,最大20,队列100 ThreadPoolExecutor pool = new ThreadPoolExecutor( 8, 20, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy() // ← 拒绝策略 ); // CallerRunsPolicy 的行为:线程池满了 → 让提交任务的线程自己执行 // 听起来不错?但问题是: // // 如果是 HTTP 请求线程提交的异步任务 → // 线程池满了 → HTTP线程自己去跑任务(比如5秒的导出报表)→ // HTTP线程被阻塞5秒!→ 接口超时!→ 用户体验极差! // // 更好的做法:用有界队列 + 自定义拒绝策略(记录日志+返回降级结果) // ✅ 推荐:自定义拒绝策略 new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { log.warn("线程池已满,任务被拒绝!队列大小={}, 活跃线程数={}", executor.getQueue().size(), executor.getActiveCount()); // 可以:返回降级结果 / 放入MQ延迟处理 / 发告警 } }
🔍 上下文丢失 — 让同事去办事但忘了告诉他客户名字
类比:你让同事去办一个客户的事,但忘了告诉他客户名字——
  · 你自己知道客户叫"张三"(主线程的ThreadLocal里有)
  · 但同事不知道(异步线程里ThreadLocal是空的)
  · 结果同事办不了事 → 上下文丢失
💀 场景3:Spring Security 上下文在异步线程中丢失
// ❌ SecurityContext 丢失 @GetMapping("/order") public String createOrder() { // 主线程中:SecurityContextHolder 能拿到当前用户 String currentUser = SecurityContextHolder.getContext() .getAuthentication().getName(); // ✅ 主线程能拿到 "张三" CompletableFuture.supplyAsync(() -> { // 异步线程中:SecurityContextHolder 是空的! String user = SecurityContextHolder.getContext() .getAuthentication().getName(); // ❌ NPE! return orderService.createOrder(user); }); return "ok"; } // ✅ 修复方式1:手动传递上下文 String currentUser = SecurityContextHolder.getContext() .getAuthentication().getName(); // 在主线程先取出来 CompletableFuture.supplyAsync(() -> { return orderService.createOrder(currentUser); // ← 直接传参 }); // ✅ 修复方式2:用 DelegatingSecurityContextExecutorService ExecutorService securedPool = new DelegatingSecurityContextExecutorService( Executors.newFixedThreadPool(8) ); // ← 自动把 SecurityContext 传到异步线程
💀 场景4:MDC 日志追踪ID在异步线程中丢失
// ❌ 主线程设了 traceId,但异步线程里没有 @GetMapping("/order") public String createOrder() { MDC.put("traceId", UUID.randomUUID().toString()); CompletableFuture.supplyAsync(() -> { log.info("处理订单"); // 日志输出:[traceId=] 处理订单 ← traceId 是空的! // 在ELK里搜索不到这条日志! return "ok"; }); return "ok"; } // ✅ 修复:手动传递 MDC Map<String, String> contextMap = MDC.getCopyOfContextMap(); // 在主线程复制 CompletableFuture.supplyAsync(() -> { if (contextMap != null) { MDC.setContextMap(contextMap); // ← 在异步线程恢复 } try { log.info("处理订单"); // [traceId=abc123] 处理订单 ✅ return "ok"; } finally { MDC.clear(); // ← 清理,防止线程复用时的上下文污染 } });
线程池耗尽 & 上下文丢失总结
① CompletableFuture 不传线程池 → 共享commonPool → 一个慢任务拖垮所有
② CallerRunsPolicy → 主线程阻塞 → 接口超时
③ Spring Security / MDC / RequestScope 基于 ThreadLocal → 异步线程自动丢失
④ 修复模式:主线程取值 → 方法参数传递 → 异步线程中恢复
⑤ 生产环境:每个业务用自己的线程池,互不影响
🏭 P10: 生产场景 — 异步改造实战
🛒 场景1:下单接口太慢 — 串行→并行
// ❌ 改造前:3个查询串行执行,总耗时 450ms @PostMapping("/order") public OrderVO createOrder(CreateOrderReq req) { String stock = stockService.check(req.getProductId()); // 200ms String coupon = couponService.check(req.getUserId()); // 150ms String address = addressService.get(req.getUserId()); // 100ms // 总耗时:200+150+100 = 450ms return buildOrderVO(stock, coupon, address); } // ✅ 改造后:3个查询并行,总耗时 200ms @PostMapping("/order") public OrderVO createOrder(CreateOrderReq req) { CompletableFuture<String> stockFuture = CompletableFuture .supplyAsync(() -> stockService.check(req.getProductId()), bizPool); CompletableFuture<String> couponFuture = CompletableFuture .supplyAsync(() -> couponService.check(req.getUserId()), bizPool); CompletableFuture<String> addressFuture = CompletableFuture .supplyAsync(() -> addressService.get(req.getUserId()), bizPool); // 等待全部完成 CompletableFuture.allOf(stockFuture, couponFuture, addressFuture).join(); return buildOrderVO( stockFuture.join(), couponFuture.join(), addressFuture.join() ); // 总耗时:max(200, 150, 100) = 200ms,提速 55%! }
改造前(串行)
0ms ├─ 查库存 200ms
200ms ├─ 查优惠券 150ms
350ms ├─ 查地址 100ms
450ms └─ 返回结果
总耗时: 450ms
改造后(并行)
0ms ├─ 查库存 200ms ┐
0ms ├─ 查优惠券 150ms ├ 同时跑!
0ms ├─ 查地址 100ms ┘
200ms └─ 返回结果
总耗时: 200ms(↓55%)
📊 场景2:报表导出 — 同步→异步+轮询
// ❌ 改造前:同步导出,大数据量直接超时 @GetMapping("/report/export") public void exportReport(HttpServletResponse response) { List<ReportData> data = reportService.queryAll(); // 查10万条,5秒 byte[] excel = excelService.generate(data); // 生成Excel,3秒 // 总耗时8秒,HTTP请求很可能超时! response.getOutputStream().write(excel); } // ✅ 改造后:异步生成+轮询下载 // 第1步:提交导出任务,立即返回任务ID @PostMapping("/report/submit") public TaskVO submitExport() { String taskId = UUID.randomUUID().toString(); // 异步生成报表 CompletableFuture.runAsync(() -> { List<ReportData> data = reportService.queryAll(); byte[] excel = excelService.generate(data); cacheService.put(taskId, excel, 30, TimeUnit.MINUTES); // 缓存30分钟 taskService.updateStatus(taskId, "DONE"); }, reportPool).exceptionally(ex -> { taskService.updateStatus(taskId, "FAILED"); log.error("报表生成失败", ex); return null; }); return new TaskVO(taskId, "PROCESSING"); } // 第2步:前端轮询任务状态 @GetMapping("/report/status/{taskId}") public TaskVO getTaskStatus(String taskId) { String status = taskService.getStatus(taskId); return new TaskVO(taskId, status); } // 第3步:下载 @GetMapping("/report/download/{taskId}") public void download(String taskId, HttpServletResponse response) { byte[] excel = cacheService.get(taskId); response.getOutputStream().write(excel); }
📨 场景3:消息发送 — 同步→异步批量
// ❌ 改造前:同步逐条发邮件 public void sendNotification(List<String> users) { for (String user : users) { // 1000个用户 emailService.send(user, content); // 每封200ms } // 总耗时:1000 × 200ms = 200秒!完全不可接受 } // ✅ 改造后:CompletableFuture 并行 + 批次控制 public void sendNotification(List<String> users) { // 分批:每批50个用户 List<List<String>> batches = Lists.partition(users, 50); for (List<String> batch : batches) { List<CompletableFuture<Void>> futures = batch.stream() .map(user -> CompletableFuture.runAsync( () -> emailService.send(user, content), emailPool )) .collect(Collectors.toList()); // 等待当前批次全部完成,再处理下一批 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .join(); } // 总耗时:20批 × 200ms = 4秒(从200秒→4秒,提速50倍!) }
⏰ 场景4:定时任务优化 — 串行→并行
// ❌ 改造前:5个统计任务串行执行 @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点 public void dailyStats() { statsService.calcOrderStats(); // 30秒 statsService.calcUserStats(); // 20秒 statsService.calcProductStats(); // 15秒 statsService.calcRevenueStats(); // 25秒 statsService.calcTrafficStats(); // 10秒 // 总耗时:100秒 } // ✅ 改造后:5个任务并行执行 @Scheduled(cron = "0 0 2 * * ?") public void dailyStats() { CompletableFuture<?>[] tasks = new CompletableFuture[]{ CompletableFuture.runAsync(() -> statsService.calcOrderStats(), statsPool), CompletableFuture.runAsync(() -> statsService.calcUserStats(), statsPool), CompletableFuture.runAsync(() -> statsService.calcProductStats(), statsPool), CompletableFuture.runAsync(() -> statsService.calcRevenueStats(), statsPool), CompletableFuture.runAsync(() -> statsService.calcTrafficStats(), statsPool), }; CompletableFuture.allOf(tasks).join(); // 总耗时:max(30,20,15,25,10) = 30秒(从100秒→30秒,提速70%) // 别忘了异常处理! for (CompletableFuture<?> task : tasks) { try { task.join(); } catch (CompletionException e) { log.error("统计任务失败", e.getCause()); } } }
4个生产场景改造总结
① 下单接口:串行3个查询→并行 → 450ms→200ms
② 报表导出:同步→异步+轮询 → 不再超时
③ 消息发送:逐条串行→分批并行 → 200秒→4秒
④ 定时任务:5个统计串行→并行 → 100秒→30秒

通用模式:找出互不依赖的步骤 → CompletableFuture.allOf 并行执行 → join 汇总
🔥 全文总结 — 异步编程核心要点

怎么写
① 简单异步(不用返回值)→ @Async
② 链式编排(有依赖关系)→ CompletableFuture
③ 并行查询(互不依赖)→ CompletableFuture.allOf
永远传自定义线程池,不要用默认的 commonPool

多线程注意什么
⑤ 竞态条件:共享变量 + 写 → 用 Atomic/锁
⑥ 死锁:固定加锁顺序 + tryLock 超时
⑦ 可见性/有序性:状态标志用 volatile

异步任务注意什么
⑧ 异常被吞:永远加 exceptionally / handle / whenComplete
⑨ @Async 的5大坑:同类调用/异常吞/无界池/事务失效/返回值
⑩ 上下文丢失:ThreadLocal(Security/MDC)不会自动传到异步线程