Skip to content

Kotlin 协程挂起恢复与并发

一句话定义

Kotlin 协程是跑在线程池之上的可挂起、可恢复的任务(不是 OS 线程):你用顺序的 suspend 代码写异步;编译器把它改写成带 label状态机,用 Continuation 把局部变量和「下一条该执行的语句」存进对象;运行时(kotlinx.coroutines)在挂起点释放线程,完成后再 resume,并由 Dispatcher 决定接着在哪个线程执行。

层面一句话
编译期suspend → 状态机 + Continuation:异步逻辑不再占满整条线程栈,断点保存在堆里
运行期挂起 = 任务让出工人;恢复 = 调度器把任务接回某条线程继续跑
与线程多协程可复用少量线程;同一条协程withContext(IO) 等切换后,生命周期里也可能先后跑在不同线程上

可运行验证:coroutine-execution-flow(Main → IO → Main 线程切换)

代码索引

主题Lab 说明源码
Java volatile / Atomic* / synchronized(对照)volatile-vs-atomic-vs-lockVolatileVsAtomicVsLock.java
Java 并发总览Java 并发模型总览
Kotlin 协程执行流(Main → IO → Main)coroutine-execution-flowCoroutineExecutionFlowDemo.kt / AdvancedCoroutinePatternsDemo.ktasync/await / Default / supervisorScope / timeout / semaphore) / ManualCoroutineEquivalent.kt
Kotlin 协程 Mutex / StateFlowcoroutine-mutex-stateflow · Mutex 与 Job / Deferred 专题CoroutineMutexStateFlowDemo.kt / MutexDemo.kt

前置对照CAS 基础 · Java volatile / 锁 §3~§4


为什么需要(与 Java 原生并发对比)

在传统 Java / Android 并发里:

痛点典型写法后果
阻塞线程Thread.sleep()Future.get()、锁内等 IO占住 OS 线程;主线程上易 ANR
回调嵌套Retrofit enqueue、多层 Callback可读性差、取消与错误路径难统一
生命周期失控new Thread { ... }、无父任务的 GlobalScope页面销毁后仍跑、泄漏与竞态

协程要解决的核心是:异步等待时不白白占着线程,并用 结构化并发CoroutineScope / Job)管理任务树。

需要澄清的误区(对应下文各节):

  • 挂起 ≠ 一定不占 CPU:在 Dispatchers.Main 上跑重计算仍会卡 UI;挂起的是「等网络/等 delay」这类可协作让出线程的点。
  • StateFlow / Mutex 不是 volatile / synchronized 的 1:1 替换:语义不同,是协程世界里的状态广播挂起互斥,Java 原语在 JNI、旧库、非协程代码里仍常见。

形象理解:服务员模型(挂起 vs 阻塞)

text
阻塞(Thread.sleep / synchronized 等锁):
  服务员(OS 线程)站在桌前干等客人吃完 → 这一桌期间不能服务别人

挂起(suspend):
  客人说「菜还没好」(网络未返回)
  → 服务员记下「这桌挂起、回来继续」(Continuation)
  → 先去服务其他桌(线程去跑别的协程或 UI)
  → 厨房好了再回来接着上菜(resume)

协程是任务;线程是执行协程的工人。一个工人可以先后服务很多「挂起过的桌」(多协程复用少量线程)。


底层机制:协程如何实现挂起 / 恢复

1. 编译器:把 suspend 变成状态机

你写的:

kotlin
suspend fun loadUser(): User {
    val raw = api.fetchUser()   // 挂起点 1
    return parse(raw)           // 挂起点 2(若 parse 也是 suspend)
}

编译后大致等价于(概念模型,非真实字节码):

kotlin
// 编译器生成带 label 的状态机 + Continuation 参数
fun loadUser(cont: Continuation<User>): Any? {
    when (cont.label) {
        0 -> { cont.label = 1; api.fetchUser(cont) ; return COROUTINE_SUSPENDED }
        1 -> { val raw = cont.result; ...; cont.label = 2; ... }
        2 -> { return parseResult }
    }
}
概念含义
suspend标记函数可被挂起;只能在协程或其它 suspend 里调用
Continuation保存「挂起后从哪条语句、哪些局部变量继续」的续体
COROUTINE_SUSPENDED告诉运行时:线程可以走了,完成时再 resume
状态机 label第几次挂起就跳到对应 case,避免回调地狱

所以:协程不是新线程,而是带保存点的可恢复任务;线程在挂起点被调度器拿去干别的。

2. 运行时:Dispatcher + 线程池

Dispatcher典型用途底层
MainUI 更新、轻量逻辑Android 主线程 Handler
IO网络、磁盘、数据库共享线程池(可阻塞型任务也放这里,而不是 Main)
DefaultCPU 密集计算线程数 ≈ CPU 核数
kotlin
viewModelScope.launch {
    val data = withContext(Dispatchers.IO) { api.fetch() }  // 切到 IO 线程执行
    _uiState.value = data                                   // 回到 scope 默认(常是 Main)
}

withContext / launch 负责在哪个线程执行哪一段suspend 负责等的时候不占着错误的那条线程

3. 结构化并发:Scope + Job

text
viewModelScope(父 Job)
  ├── launch { loadA() }
  └── launch { loadB() }

ViewModel.onCleared() → scope 取消 → 子协程一并取消

对比 Java:GlobalScope / 裸 Thread 像「离职员工还在帮公司打电话」;viewModelScope / lifecycleScope 把任务绑在生命周期上,是 Android 上减少泄漏的关键。


核心概念速查(需要单独认识,不能一笔带过)

suspend / launch / async

API作用
suspend fun可被挂起的函数;内部可调用其它 suspend API
launch启动协程,不关心返回值(fire-and-forget),异常走 CoroutineExceptionHandler
async启动协程并返回 Deferred<T>,用 await() 拿结果(类似 Future,但在协程里挂起等待)

Mutex(协程互斥锁)

对应 Labcoroutine-mutex-stateflow · Mutex 与 Job / Deferred 专题 · CoroutineMutexStateFlowDemo.kt §2 · MutexDemo.kt

保护协程临界区;拿不到锁时 suspend 当前协程,不阻塞 OS 线程(与 synchronized 在等锁时阻塞线程不同)。

kotlin
private val mutex = Mutex()
private var cache = emptyList<User>()

suspend fun updateCache(users: List<User>) {
    mutex.withLock {
        cache = users          // 多字段一致变更放在锁内
    }
}
对比synchronizedMutex
等待锁时阻塞线程挂起协程,线程可跑别的协程
可重入(同线程可再进)(同协程再 lock 会死锁)— 见 MUTEX_AND_JOB.md §2
使用场景任意 Java/Kotlin 代码suspend 函数内的共享状态
Android 主线程锁内慢操作 → ANR 风险仍要避免在 Main 上写长临界区;只是等锁方式不同

StateFlow / SharedFlow(热流,状态广播)

对应 Labcoroutine-mutex-stateflow · §3~§4

类型特点典型用途
StateFlow<T>必有当前值;新订阅者立刻收到最新快照value 读当前态UI 单源状态:UiState、加载中/成功/失败
SharedFlow<T>无强制「当前值」;可配置 replay;适合事件一次性事件:Snackbar、导航、点击脉冲
kotlin
data class UiState(val loading: Boolean = false, val users: List<User> = emptyList())

private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

suspend fun refresh() {
    _uiState.update { it.copy(loading = true) }
    val users = withContext(Dispatchers.IO) { repo.load() }
    _uiState.update { it.copy(loading = false, users = users) }
}

和 Java 对照:

Java / 旧 AndroidKotlin 协程常见替代说明
volatile 标志位Job.isActive、取消协作取消是协程结构化能力,不是简单布尔可见性
AtomicInteger 计数仍可用 AtomicInteger;或 StateFlow.update 广播要「UI 也要看见」时用 Flow
synchronized 多字段Mutex.withLock + 不可变 copy状态对外暴露快照,减少半初始化可见
LiveDataStateFlow + repeatOnLifecycleFlow 更利于组合、背压与测试

不是volatile 消失了:JNI、旧 SDK、@Volatile 字段在纯 JVM 模块里仍合理。

withContext vs launch

想先把最小执行链打通,可先看 Lab:coroutine-execution-flow —— 只围绕 suspend fun showUser() { val user = withContext(IO) { repo.loadUser() }; render(user) } 讲清 Main → IO → Main、状态机分支、数据如何经 continuation/result 流转,以及类 Java 的 Executor + 状态机等效写法。

  • withContext(Dispatchers.IO) { }:挂起当前协程,在指定调度器执行块,结束后回到原上下文;适合顺序 IO。
  • launch:新子协程并行;要注意异常传播与 join

并发互斥与状态管理选型(汇总表)

需求场景传统 Java / JVMKotlin 协程方案要点
取消 / 是否仍运行手动 volatile + 检查coroutineContext.isActiveJob.cancel()结构化取消向下传播
单变量原子累加AtomicIntegerAtomicIntegerStateFlow.update仅计数用 Atomic 即可;要驱动 UI 用 Flow
多字段一致修改synchronized / LockMutex.withLock + 不可变 data class协程临界区优先 Mutex
UI 状态订阅LiveData / 手写观察者StateFlow + collect配合 lifecycleScope / repeatOnLifecycle
一次性 UI 事件SingleLiveEvent 等 hackSharedFlow / Channel避免旋转屏重复消费

Android 实战注意

  1. 主线程:UI 只能在 Main 更新;IO 用 withContext(IO)flowOn(IO)
  2. ScopeviewModelScope / lifecycleScope,避免 GlobalScope
  3. collect 生命周期:用 repeatOnLifecycle(STARTED),避免后台无效 collect 与泄漏。
  4. 不要用 Flow 替代所有锁:与 Java 模块、Room 事务、第三方回调交界仍可能要用 synchronized / 平台 API。
  5. 协程 ≠ 免费并行:CPU 密集任务应用 Default 并限制并发度,否则仍抢 CPU。

常见坑

后果方向
Maindelay 以外的重计算卡顿 / ANRwithContext(Default)
GlobalScope.launch泄漏、无法取消绑定 viewModelScope
StateFlowvolatile误用 API状态用不可变快照 + update
SharedFlowStateFlow 显示 UI旋转屏丢状态界面态用 StateFlow
async 异常未 await静默失败supervisorScope + 统一处理
Mutex 锁内调用 Main 慢操作逻辑仍慢锁内只改内存,IO 在锁外

与相近概念对比

概念关系
Java Futureasync/await 类似,但协程挂起不阻塞线程
RxJava同为响应式;协程 + Flow 更贴近 Kotlin 语法
Dart Future / async-await同属挂起续体模型;Dart 单线程 event loop,Kotlin 多线程 Dispatcher
Flutter Isolate内存隔离;Kotlin 默认协程共享内存,需 Mutex / 不可变状态

对应实验

Lab说明状态
volatile-vs-atomic-vs-lockJava 原语对照已有
coroutine-execution-flowMain → IO → Main 执行链 + async/await / Default / supervisorScope / 超时降级 / 并发限制 / Kotlin 手写状态机等效实现已有
coroutine-mutex-stateflowMutex + StateFlow + SharedFlow 可运行示例已有

复习检查题

  1. 协程「挂起」和线程「阻塞」最大区别是什么?

    :挂起时协程任务让出执行权,OS 线程可去跑其它协程;阻塞时线程本身卡在等待上。协程是轻量任务,线程是执行载体。

  2. suspend 函数在编译后大致变成什么结构?

    :带 Continuation 参数的状态机label 标记挂起点),返回 COROUTINE_SUSPENDED 表示已挂起,完成后通过 resume 从断点继续。

  3. StateFlowSharedFlow 各适合什么?

    StateFlow 持有当前状态快照,适合 UI 界面态(加载中/列表数据)。SharedFlow 适合事件流(Toast、导航、一次性信号),默认不保证新订阅者拿到历史「当前值」。

  4. 为什么协程里推荐 Mutex 而不是在主流程里用 synchronized

    :等 synchronized阻塞线程Mutex 在协程中等待时挂起协程,线程可复用。在 Android 上减少主线程或池线程被无谓占满的风险(锁内仍不能做长 IO)。

  5. Mutexsynchronized 在「重入」上有什么不同?

    synchronized / ReentrantLock 可重入(同线程可再进同一把锁)。Mutex 不可重入——同协程在 withLock 内再次 withLock 同一实例会挂起自己、永远等不到锁,形成死锁。分层 API 应「一层锁」或拆 *Locked() 无锁内层方法。详见 MUTEX_AND_JOB.md §2

  6. Dispatchers.Main / IO / Default 各放什么任务?

    Main:UI 与轻量逻辑;IO:网络、磁盘、阻塞 IO;Default:CPU 密集计算。重活放错 Dispatcher 仍会卡 UI 或浪费线程。

  7. 用了协程是否就不需要 volatile / AtomicInteger

    不是。协程解决异步结构与线程占用;跨线程单变量原子、Java 互操作、非 suspend 代码仍可能需要 volatile / Atomic*。UI 状态更常交给 StateFlow + 不可变对象。

速记

  • 协程 = 可挂起恢复的任务;线程 = 跑任务的工人;suspend = 编译器状态机 + Continuation。
  • 挂起释放线程,阻塞占住线程;Main 上重 CPU 仍会卡。
  • UI 态 → StateFlow;一次性事件 → SharedFlow;协程临界区 → Mutex.withLock(不可重入,勿嵌套同实例 withLock)。
  • Scope 绑生命周期;IO 用 withContext(IO);Java 对照 → volatile-vs-atomic-vs-lock;可运行 → coroutine-mutex-stateflow。