系统解析 Java CompletableFuture 框架下的多线程的用法、线程安全原理、乐观锁、悲观锁、防超卖方案选择指南,以及实际项目中的高并发最佳实践。
一、CompletableFuture 基础认知
1.1 它是什么?
CompletableFuture 是 Java 8 引入的异步编程核心组件,基于 Future 和 CompletionStage 两大接口构建,提供:
-
异步计算:任务后台执行,不阻塞主线程
-
链式调用:优雅组合多个异步任务
-
异常处理:完整的异常捕获与恢复
-
结果组合:支持合并多个 Future
它是 Java 原生异步任务编排的最佳工具。
1.2 常用创建方式
// 无返回值异步任务
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("执行异步任务");
});
// 有返回值异步任务
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "异步结果");
// 使用自定义线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "异步任务", executor);
二、线程安全原理与防超卖方案解析
2.1 CompletableFuture 的线程安全来自哪里?
内部依赖两大策略:
✔ 无锁 CAS(Compare-And-Swap)
volatile Object result;
final boolean internalComplete(Object r) {
return UNSAFE.compareAndSwapObject(this, RESULT, null, r);
}
意义:结果只会被成功设置一次,后续线程竞争失败。
✔ Treiber Stack(无锁栈)管理回调链
每一次回调注册都通过无锁方式入栈,保证并发安全。
✔ volatile 保证可见性
线程间结果及时可见。
结论:
CompletableFuture 只保证自身状态的线程安全,不保证你业务数据的线程安全。
2.2 防超卖方案全解析(高并发核心问题)
方案一:synchronized(悲观锁)
public synchronized boolean purchase() {
if (stock > 0) {
stock--;
return true;
}
return false;
}
-
优点:实现简单
-
缺点:吞吐量差,严重阻塞
-
适用:低并发、小业务
方案二:乐观锁(版本号)
实体类对应数据库 库存表中的version字段,mybatis-plus就会对齐,将version当做版本号处理
@Version
private Integer version;
典型写法:
for (int i = 0; i < 3; i++) {
Product product = productMapper.selectById(productId);
if (product.getStock() <= 0) return false;
product.setStock(product.getStock() - 1);
int rows = productMapper.updateById(product); // MP 自动判断版本
if (rows > 0) return true; // 成功
}
-
优点:性能好,尤其读多写少
-
缺点:会出现大量重试
-
适用:中等并发、冲突少的场景
方案三:数据库原子操作(推荐)
@Update("UPDATE product SET stock = stock - 1 WHERE id = #{id} AND stock > 0")
int deductStockAtomic(Long id);
-
优点:强一致 + 高性能 + 无锁
-
缺点:SQL 需自定义
-
适用:常规秒杀、高并发扣减
这是目前 90% 电商场景的首选方案。
方案四:Redis 原子操作 + 异步落库(极致性能)
Long stock = redisTemplate.opsForValue().decrement(key);
if (stock >= 0) {
CompletableFuture.runAsync(() -> syncToDatabase(productId, stock));
return true;
} else {
redisTemplate.opsForValue().increment(key); // 回滚
return false;
}
-
优点:性能极高
-
缺点:一致性保障复杂
-
适用:超高并发(双11级别)
2.3 实战场景方案选择
| 场景 | 推荐方案 | QPS | 难度 |
|---|---|---|---|
| 普通电商 | 乐观锁 | 1k–5k | 中 |
| 大促秒杀 | 数据库原子操作 | 5k–10k | 低 |
| 超高并发秒杀 | Redis 原子 + 落库 | 10k+ | 高 |
| 复杂业务多校验 | 悲观锁或分段锁 | 500–1k | 中 |
三、CompletableFuture 使用技巧
3.1 循环创建异步任务的正确姿势
错误(闭包问题):
for (int i = 0; i < 10; i++) {
CompletableFuture.runAsync(() -> System.out.println(i));
}
正确:
for (int i = 0; i < 10; i++) {
final int id = i;
futures.add(CompletableFuture.runAsync(() -> System.out.println(id)));
}
CompletableFuture.allOf(futures...).join();
3.2 全面的异常处理
exceptionally:异常恢复
future.exceptionally(ex -> "默认值");
handle:成功/失败统一处理
future.handle((r, ex) -> ex == null ? r : "异常处理");
whenComplete:类似 finally
future.whenComplete((r, ex) -> log.info("结束"));
3.3 多任务组合
thenCompose:前后依赖
getUserAsync(id).thenCompose(user -> getOrderAsync(user));
thenCombine:并行任务合并
u.thenCombine(o, (u1, o1) -> u1 + o1);
allOf:等待全部完成
CompletableFuture.allOf(f1, f2, f3).join();
anyOf:任意一个完成
四、高并发场景最佳实践
4.1 秒杀系统示例
@Transactional
public CompletableFuture<Boolean> purchase(Long pid, Long uid) {
return CompletableFuture.supplyAsync(() -> {
int rows = jdbc.update("UPDATE ... stock > 0", pid);
if (rows > 0) {
createOrder(uid, pid);
CompletableFuture.runAsync(() -> sendMsg(uid));
return true;
}
return false;
}, flashSalePool);
}
4.2 性能优化策略
1. 线程池规划
// CPU 密集
newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// IO 密集
newFixedThreadPool(50);
2. 限流(信号量)
Semaphore sem = new Semaphore(100);
3. 监控 + 降级
五、常见问题解答
Q1:CompletableFuture 自身是否线程安全?
A:是,但它不保证你业务数据的线程安全。
Q2:循环创建 supplyAsync 会产生多线程吗?
A:会。但可能受线程池大小限制。
Q3:一定要 allOf().join() 吗?
-
需要等待所有结果 ⇒ 必须
-
后台任务(发通知等)⇒ 不需要
Q4:如何选择锁?
-
低并发:synchronized
-
中并发:乐观锁
-
高并发:数据库原子操作
-
超高并发:Redis
Q5:程序退出时异步任务未完成?
使用 ShutdownHook:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
executor.shutdown();
executor.awaitTermination(...);
}));
六、总结(高质量可复用)
CompletableFuture 的使用原则
-
异步编排交给 CompletableFuture
-
线程安全交给数据库原子操作
-
闭包变量要处理好
-
异常不能静默失败
-
线程池必须正确配置
-
高并发下推荐 Redis/原子 SQL
实战口诀(优化版)
异步任务用 Future,链式编排 Completable; 线程安全要分层,业务数据自己管; 循环创建要注意,闭包变量别踩坑; 高并发防超卖,原子 SQL 最稳健; 线程池要配置,监控降级别缺省。
🔥 防超卖四大方案(通俗易懂版)
在高并发场景下,“库存扣减”是最容易出现超卖的业务。下面从简单到复杂,讲清四种主流方案的原理、优缺点、以及为什么它们能保证不超卖。
① 乐观锁:大家都抢,但成功的人少(需要重试)
核心思想:
每条数据都有一个 version 版本号。
每个线程读取数据后,都带着一个“旧版本号”去更新。
如果这个版本号和数据库里的当前版本号不一致 ⇒ 说明有其他线程抢先更新过了,该线程更新失败,需要重试。
通俗理解:
每个线程都带着一张写着 “v1” 的票去修改库存:
-
如果数据库当前版本还是 v1 ⇒ 你赢了,修改成功
-
如果数据库已经变成 v2 ⇒ 说明别人抢先更新了 ⇒ 你要重新读取再试
你写的内容优化版:
每个线程都读到一个版本号(例如 v1),修改时要求数据库版本仍是 v1,才允许更新成功。
若修改期间,版本号被其他线程更新成 v2,则当前线程必须重新读取(得到 v2)然后再尝试,直到成功。
能保证不超卖吗?
✔ 能,因为每次更新都要求版本一致,否则就失败。
✖ 缺点:大量冲突时重试成本高。
② 悲观锁:一次只能一个人进去处理(性能差)
核心思想:
给数据加锁,其他线程必须等待锁释放。
通俗理解:
就像一个卫生间:
-
A进去后把门反锁
-
B 只能排队等 A 出来
-
A 不出来,所有人都别想进去
你写的内容优化版:
悲观锁会阻塞其他线程:只要 A 线程还在修改,B、C、D 都只能排队等待。如果并发量大,性能会急剧下降。
能保证不超卖吗?
✔ 能
✖ 性能最差,不适合秒杀
③ 数据库原子操作:一条 SQL 解决所有问题(最推荐)
核心思想:
直接利用数据库的原子执行能力,一条 SQL 完成库存扣减,要么成功,要么失败。
例如:
UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0;
通俗理解:
一个人去仓库拿货:
-
仓库管理员一次性检查库存是否大于 0
-
大于 0 ⇒ 给你减一 ⇒ 成功
-
等于 0 ⇒ 不给你 ⇒ 失败
这个过程 整个是原子的(不可分割),绝不会出现并发读写造成超卖。
你写的内容优化版:
数据库能保证一条 update 语句的原子性,只要条件满足才能扣减成功。如果库存不大于 0,SQL 不会执行,从而天然防止超卖。
能保证不超卖吗?
✔ 绝对能
✔ 性能高
✔ 电商 90% 都用这个
✖ 需要写原生 SQL
④ Redis 原子操作 + 异步落库(极致性能)
核心思想:
Redis 的 DECR 操作是原子的,多个线程同时执行也不会出错。
Long stock = redisTemplate.opsForValue().decrement(key);
执行结果:
-
stock >= 0⇒ 秒杀成功(还有库存) -
stock < 0⇒ 超卖了,需要回滚(increment)
你写的内容优化版:
将库存和商品 ID 存成 Redis 的 key-value,例如:
stock:1001 = 50
当用户参与秒杀时,先在 Redis 执行DECR进行扣减(原子操作)。如果扣减成功(值 ≥ 0):
→ 表示抢购成功
→ 然后通过异步线程把数据落库(库存减一、订单加一)如果扣减失败(值 < 0):
→ Redis 回滚(INCR)
→ 表示抢购失败
为什么性能如此高?
因为 Redis 是纯内存 + 单线程架构,天然保证同一个 key 的访问串行化。
能保证不超卖吗?
✔ Redis 层一定不会超卖
✖ 最终一致性要自己保证(落库失败如何处理?补偿?重试?)
🔔 四大方案极致对比总结
| 方案 | 是否阻塞 | 是否会超卖 | 性能 | 适用场景 |
|---|---|---|---|---|
| 悲观锁 | ✔ 阻塞 | ❌ 不会 | ❌ 最差 | 小系统、低并发 |
| 乐观锁 | ❌ 不阻塞 | ❌ 不会 | ⭐ 中等 | 读多写少、中等并发 |
| DB 原子 SQL | ❌ 不阻塞 | ❌ 不会 | ⭐⭐ 高 | 大部分电商场景 |
| Redis DECR | ❌ 不阻塞 | ❌ Redis 层不会 | ⭐⭐⭐ 最高 | 超高并发秒杀 |
📘 最通俗易懂的一句话总结
悲观锁靠“互斥”,乐观锁靠“版本号”,原子 SQL 靠“数据库”,Redis DECR 靠“内存单线程”。
选择哪个,看你业务的 QPS 和一致性需求。

被折叠的 条评论
为什么被折叠?



