Android 面试系列 : 协程为何比线程高效

很多人认为协程比线程高效,是因为:

  • 更轻量;
  • 更少的线程;
  • 用户态切换;
  • 一个线程运行几十万个协程。

这些怎么说呢,一言难尽。

其实这些并不是 Kotlin 协程改变 Android 开发方式的核心原因。

因为:

线程池同样可以复用线程;

Netty 同样可以处理海量连接;

Node.js 很早就实现了单线程高并发。

协程真正高效的地方其实是:

  • 它让异步重新变成了线性代码;
  • 它让线程阻塞变成协程挂起;
  • 它让线程之间的同步协作变成协程之间的协作;
  • 它减少了大量为了组织异步流程而引入的 JUC 同步器。

而这些,才是协程真正改变异步开发方式的地方。


一、线程时代最大的成本,其实是异步复杂度

很多人认为线程模型最大的成本是:

阻塞。

但对于绝大多数 Android 开发者来说,真正痛苦的其实是:

异步会打碎程序的控制流。

例如经典的回调写法:

kotlin 复制代码
loginAsync { user ->
    queryProfileAsync(user.id) { profile ->
        queryVipAsync(profile.id) { vip ->
            updateUi(vip)
        }
    }
}

业务流程其实非常简单:

text 复制代码
登录
↓
查询用户信息
↓
查询会员信息
↓
更新 UI

但代码结构却变成:

text 复制代码
callback
 └── callback
      └── callback
           └── callback

于是各种问题开始出现:

  • 异常处理困难;
  • 生命周期难管理;
  • 状态同步复杂;
  • 取消逻辑难维护;
  • 阅读成本急剧上升。

真正消耗开发效率的,从来不是线程本身,

而是:

异步代码失去了线性表达能力。


二、线程时代想写同步代码,往往需要各种 JUC

面对 Callback Hell,我们自然会产生一个想法:

能不能把异步代码重新写成同步代码?

线程时代当然可以。

但通常需要借助各种同步工具。

例如:

java 复制代码
CountDownLatch latch = new CountDownLatch(1);

loginAsync(user -> {
    result = user;
    latch.countDown();
});

latch.await();

User user = result;

或者:

java 复制代码
User user = future.get();

或者:

java 复制代码
User user = blockingQueue.take();

本质都是:

text 复制代码
异步回调
        │
        ▼
JUC 同步器(CountDownLatch、Future、BlockingQueue......)
        │
        ▼
线程阻塞等待
        │
        ▼
回调通知线程继续执行

代码确实重新变成了:

java 复制代码
User user = login();
Profile profile = queryProfile(user.id);
Vip vip = queryVip(profile.id);

updateUi(vip);

但是代价也非常明显。

为了获得同步代码的阅读体验,我们不得不:

  • 阻塞当前线程;
  • 使用额外线程承载这些等待任务,防止 ANR;
  • 引入大量 JUC 同步工具来协调异步流程。

于是项目里开始出现越来越多的:

  • CountDownLatch
  • Future
  • CompletableFuture
  • BlockingQueue
  • wait / notify
  • synchronized
  • ReentrantLock

很多时候:

这些工具并不是业务真正需要的。

它们存在的目的,仅仅只是:

把异步回调重新缝合成线性代码。

更重要的是,这些同步工具本身也存在额外成本。

例如:

  • CountDownLatch 底层依赖 AQS;
  • Future.get() 会阻塞线程;
  • BlockingQueue 内部大量依赖 Lock、Condition 或 CAS;
  • wait / notify 依赖对象监视器;
  • synchronized、ReentrantLock 会带来锁竞争。

也就是说:

线程时代为了获得同步代码,不仅付出了线程阻塞的代价,还付出了大量同步器协作的代价。


三、协程最大的价值:异步同步化并且是非阻塞的

而协程第一次优雅地解决了这个问题。

kotlin 复制代码
val user = login()
val profile = queryProfile(user.id)
val vip = queryVip(profile.id)

updateUi(vip)

看起来是同步代码:

text 复制代码
A → B → C → D

实际上运行过程却是:

text 复制代码
login()
↓
挂起协程
↓
释放线程
↓
等待网络返回
↓
恢复协程
↓
继续执行下一行代码

整个过程中:

没有:

  • CountDownLatch
  • Future.get()
  • CompletableFuture.join()
  • BlockingQueue
  • wait / notify
  • synchronized
  • ReentrantLock

甚至没有:

kotlin 复制代码
Thread.sleep()

因为协程并不是依赖这些同步工具等待结果。

suspend 的本质,是编译器生成的 Continuation(续体)+ 状态机

当异步任务没有完成时:

text 复制代码
保存执行现场
↓
挂起协程
↓
释放线程
↓
异步任务完成
↓
resume()
↓
恢复状态机继续执行

等待的是:

text 复制代码
协程

而不是:

text 复制代码
线程

因此:

协程并不是把 Callback 变成了 CountDownLatch,而是直接把 Callback 变成了 Continuation。

整个异步流程重新回到了线性表达。

协程完成了一件线程时代很难优雅做到的事情:

让异步代码拥有同步代码的阅读体验。

同时又保持:

非阻塞执行。

这可能才是 Kotlin 协程相比传统线程模型最大的价值。

因为它第一次真正实现了:

线性表达 + 非阻塞执行。


四、协程不仅减少锁竞争,更减少了同步器的使用

线程模型下:

多个线程同时访问共享资源时:

  • synchronized
  • ReentrantLock
  • Condition
  • wait / notify
  • BlockingQueue

几乎不可避免。

不仅如此。

为了把异步重新组织成同步流程,还需要:

  • CountDownLatch
  • Future
  • CompletableFuture
  • Semaphore

也就是说:

线程时代大量代码,其实都是围绕:

线程之间如何协作。

而不是:

业务本身。

协程改变了这一点。

例如:

kotlin 复制代码
val mutex = Mutex()

mutex.withLock {
    updateData()
}

如果锁已经被占用:

text 复制代码
挂起协程
↓
释放线程
↓
等待恢复

等待的是:

text 复制代码
协程

而不是:

text 复制代码
线程

因此:

  • 不容易产生线程池饥饿;
  • 不容易出现大量 WAITING 线程;
  • 锁竞争造成的资源浪费更少;
  • 调度压力明显减小。

不过,协程真正厉害的地方,并不是 Mutexsynchronized 更快。

而是:

很多原本需要加锁的问题,在协程模型下根本就不需要加锁。

例如:

text 复制代码
多个协程
      │
      ▼
 Channel
      │
      ▼
 单个消费者
      │
      ▼
 修改共享状态

或者:

text 复制代码
StateFlow

SharedFlow

Actor

共享状态逐渐变成:

单一拥有者(Single Owner)

多个任务之间:

通过消息传递进行协作。

而不是:

多个线程竞争同一把锁。

因此 Kotlin 世界越来越倾向于:

  • Mutex
  • Channel
  • Flow
  • Actor
  • Semaphore

而不是:

  • synchronized
  • ReentrantLock
  • BlockingQueue
  • wait / notify

因为:

协程时代强调的是协作,而不是竞争。

真正减少的,不只是锁竞争。

更重要的是:

很多原本必须依赖锁和同步器解决的问题,在协程模型下根本就不需要出现。


五、结构化并发进一步降低了复杂度

线程时代:

任务生命周期和业务生命周期往往是分离的。

例如:

text 复制代码
页面销毁了
线程还在执行。

于是经常出现:

  • 页面退出后回调回来更新 UI;
  • Future 无法取消;
  • 后台任务泄漏;
  • 生命周期错乱。

而协程:

text 复制代码
ViewModelScope
    ├── task1
    ├── task2
    └── task3

父协程取消:

text 复制代码
所有子协程自动取消。

生命周期天然绑定。

大量同步和状态管理问题因此消失。

这也是 Kotlin 协程另一个容易被忽视的优势:

结构化并发降低了系统复杂度。


六、协程并没有消灭回调

很多人第一次接触协程时都会有一种感觉:

suspend 函数没有回调。

其实并不是。

如果反编译一个 suspend 函数:

kotlin 复制代码
suspend fun login(): User

最终大致会变成:

java 复制代码
Object login(Continuation<? super User> continuation)

本质仍然是:

text 复制代码
请求完成
↓
回调 Continuation
↓
恢复协程执行

区别仅仅在于:

以前:

text 复制代码
开发者维护回调链。
开发者维护状态机。

现在:

text 复制代码
编译器维护回调链。
编译器维护状态机。

所以从某种意义上来说:

协程并没有消灭回调。

它只是:

把回调隐藏到了编译器和运行时之中。


七、协程真正优化的不是线程,而是异步编程

很多文章喜欢说:

协程比线程快。

实际上这并不准确。

真正执行代码的始终都是线程。

协程优化的从来不是:

text 复制代码
CPU 计算速度

而是:

text 复制代码
异步程序的组织成本

它减少的是:

  • Callback Hell;
  • 状态机维护成本;
  • JUC 同步器的使用;
  • 锁竞争成本;
  • 阻塞等待成本;
  • 生命周期管理成本。

因此更准确的说法应该是:

协程不是让程序运行得更快。

而是:

让异步程序变得更简单、更稳定、更容易维护。


八、总结

线程池解决的是:

text 复制代码
线程复用问题。

协程解决的是:

text 复制代码
异步表达问题。

因此:

协程比线程高效,并不是因为它创建得更轻,也不是因为它使用了更少的线程。

真正发生变化的是:

它重新设计了异步程序的组织方式。

它让:

  • Callback 变成了 Continuation;
  • 手写状态机变成了编译器生成状态机;
  • 线程阻塞变成了协程挂起;
  • JUC 同步器变成了协程协作;
  • 线程竞争变成了消息传递与单一所有者模型。

所以,协程真正优化的,从来不是 CPU,也不是线程。

而是:

异步程序的组织成本。

它让开发者不再把大量精力浪费在:

  • 如何等待;
  • 如何同步;
  • 如何加锁;
  • 如何管理回调;
  • 如何维护状态;

而是把注意力重新放回:

业务本身。

所以,我认为Kotlin 协程真正伟大的地方,并不是它比线程快了多少。

而是它:

用同步代码的思维,编写异步程序。

用非阻塞的挂起机制,获得同步代码般的可读性。

协程并不是消灭所有异步开销,而是减少为了"让异步流程表现得像同步代码"而引入的大量同步控制成本。


协程减少的不是 Thread 的数量,而是:

  • CountDownLatch 等等待同步器;
  • Future / CompletableFuture 等异步组合机制;
  • 回调嵌套带来的复杂控制流;
  • 围绕线程同步产生的锁、状态管理和异常处理代码。

所以本质上协程比线程高效的原因是减少了异步复杂度。

参考

Kotlin 官网为什么不再强调"协程是轻量级线程"了?

从 Callback 到 Coroutines:Android 异步并发方案的演进

"结构化"这个词,本质上就是------把混乱的东西变成有组织、有规则、有边界的东西

从"调用方的如履薄冰"到"接口的天然语义":Room/DataStore/Retrofit 的启示

现代 Android 官方为什么更推荐 Repository 暴露 suspend fun,而不是在内部 launch

# Android 架构指南之Data 层不要再暴露 start/stop 了:用 Flow 接管生命周期

别再 launch(IO) 了:协程线程切换的 3隐藏反模式

从送外卖看Android Clean架构:为什么老板不需要知道外卖员开什么车

用一个小 Demo,带你入门安卓 Clean Architecture

Android 现代架构不需要事件总线

为什么我不在 Android ViewModel 中直接处理异常?

相关推荐
2501_9327502620 小时前
Android 数据持久化解析
android·java
石山代码1 天前
KMP全栈开发:从Android到AI Agent的技术演进与实践
android·人工智能
怣疯knight1 天前
kotlin安卓应用打包编译卡死的可能原因
android·kotlin
plainGeekDev1 天前
NullPointerException → Kotlin 空安全
android·java·kotlin
2501_916008891 天前
苹果上架工具怎么选 不用 Mac 上架 App Store 的几种方案
android·macos·ios·小程序·uni-app·iphone·webview
jijihusong0062 天前
KMP全栈开发教程:从Android到AI Agent
android·人工智能
_祝你今天愉快2 天前
Android Binder 驱动 - 内核驱动层源码初探
android
stevenzqzq2 天前
【无标题】
android
梦幻通灵2 天前
Notepad++格式化Json两种方案【持续更新】
android·json·notepad++