前言:同时发起 A、B、C 三个网络请求,全部完成后再串行请求 D ------ 这是移动端最常见的并发场景之一。本文通过完整可运行的代码,横向对比 5 种主流实现方案,带你看清各自的本质与取舍。
📌 场景定义
scss
[请求 A] ─┐
[请求 B] ─┼─ (并行) ──▶ 全部完成 ──▶ [请求 D] ──▶ 拿到最终结果
[请求 C] ─┘
这个"3并1串"的模式在真实业务中极为常见,比如:首页同时拉取轮播图、文章列表、置顶文章,全部就绪后再根据结果请求个性化推荐。
方案一:Java Callbacks(嵌套回调)
实现代码

⚠️ 核心问题
注意 :这段代码虽然嵌套了 4 层,但 A、B、C 三个请求实际上是串行 的,并非真正的并行,要在纯 Callback 模式下实现并行,你还需要额外维护一个计数器或 AtomicInteger,代码复杂度会进一步爆炸。
| 维度 | 评估 |
|---|---|
| 并行实现 | ❌ 需要手动计数器或者JUC,极易出错 |
| 代码可读性 | ❌ 向右无限延伸,逻辑碎片化 |
| 错误处理 | ❌ 每层都要重复 onFailure |
| 适用场景 | 仅适合单次简单异步 |
方案二:CountDownLatch(显式锁阻塞)
实现代码

⚠️ 核心问题
latch.await() 会让后台线程进入 Blocked(阻塞) 状态,线程被占用但什么工作也不做。在主线程等待,这会导致ANR。
更危险的是:任何一个分支如果忘记调用
countDown()(例如某个异常路径没覆盖到),整个流程将永久死等。
| 维度 | 评估 |
|---|---|
| 并行实现 | ✅ 真正并行 |
| 线程状态 | ❌ 阻塞(Blocked),浪费资源 |
| 健壮性 | ❌ 遗漏 countDown = 死锁 |
| 代码复杂度 | 中等 |
方案三:CompletableFuture(链式异步)
实现代码

⚠️ 核心问题
逻辑链路清晰了许多,但有两个隐患:
- 调试地狱:异常堆栈在异步链条中会丢失调用上下文,出了问题很难定位到具体是哪一步。
- 心智负担 :
thenApply/thenCompose/thenCombine/allOf------ 你需要熟练掌握这些操作符的区别才能正确使用。
| 维度 | 评估 |
|---|---|
| 并行实现 | ✅ 真正并行 |
| 线程状态 | ✅ 非阻塞(线程池驱动) |
| 调试体验 | ❌ 异步链中堆栈丢失 |
| API 学习成本 | 中等偏高 |
方案四:RxJava3(响应式流)
实现代码

⚠️ 核心问题
RxJava 非常强大,但在这个场景下有"大炮打蚊子"之嫌:
- 框架重量级:引入 RxJava3 + RxAndroid,仅为处理简单的顺序业务逻辑,成本偏高。
- 生命周期管理 :
subscribe返回的Disposable必须在onDestroy中手动dispose(),否则会内存泄漏。
kotlin
// 不处理 Disposable = 内存泄漏!
val disposable = observable.subscribe(...)
// 必须在 onDestroy 中:
disposable.dispose()
| 维度 | 评估 |
|---|---|
| 并行实现 | ✅ 真正并行(zip 操作符) |
| 表达力 | ✅ 复杂数据流处理极强 |
| 框架重量 | ❌ 引入成本高 |
| 生命周期 | ❌ 需手动管理 Disposable |
方案五:Kotlin Coroutines(协程 ⭐ 推荐)
实现代码

✅ 为什么协程是最佳答案
1. 挂起 ≠ 阻塞
await() 期间,线程不会被占用------协程被"挂起",线程可以去做其他事情。这与 CountDownLatch.await() 的死等有本质区别。
ini
CountDownLatch: 线程 ──[Blocked 死等]──────────────▶ 继续
Coroutine: 线程 ──[释放去处理其他协程]──▶ 恢复继续
2. 结构化并发,生命周期自动管理
lifecycleScope.launch 会自动将协程的生命周期绑定到 Activity,当 Activity 销毁时,所有未完成的请求会自动取消,无需任何手动清理。
3. 用线性代码表达异步逻辑
上面的协程代码读起来就像同步代码,但底层完全是异步非阻塞的。这就是协程最核心的价值:用人类最自然的线性思维,编写高性能的异步逻辑。
| 维度 | 评估 |
|---|---|
| 并行实现 | ✅ async/await |
| 线程状态 | ✅ 挂起(Suspended),不占资源 |
| 生命周期 | ✅ 自动绑定,无需手动取消 |
| 代码可读性 | ✅ 伪同步,极易维护 |
| 学习成本 | 低(语言原生支持) |
📊 五方案横向对比
| 方案 | 真正并行 | 等待机制 | 线程状态 | 生命周期管理 | 代码复杂度 | 可维护性 |
|---|---|---|---|---|---|---|
| Callbacks | ❌ | 嵌套触发 | 运行/空闲 | 手动 | 极高 | ⭐ |
| CountDownLatch | ✅ | 显式锁阻塞 | Blocked | 手动 | 中 | ⭐⭐ |
| CompletableFuture | ✅ | 回调链驱动 | 运行(线程池) | 手动 | 中高 | ⭐⭐⭐ |
| RxJava3 | ✅ | 操作符聚合 | 运行(线程池) | 手动 Disposable | 高 | ⭐⭐⭐ |
| Coroutines | ✅ | 非阻塞挂起 | Suspended | 自动 | 极低 | ⭐⭐⭐⭐⭐ |
💡 总结
每一种方案的演进,都是在解决上一代的痛点:
- Callbacks ------ 最原始,让代码横向无限延伸,并行更是一场噩梦。
- CountDownLatch ------ 解决了并行,但用阻塞线程换来的,有ANR和死锁风险。
- CompletableFuture ------ 链式调用更优雅,但调试困难,API 理解成本不低。
- RxJava3 ------ 对复杂数据流极其强大,但在简单场景下是过度设计,生命周期还要自己管。
- Kotlin Coroutines ------ 消灭了锁,消灭了回调,让代码回归线性,阅读起来顺畅。