Kotlin 协程 Coroutine

48 阅读14分钟

1、什么是协程?和线程有什么区别

答案

协程是轻量级线程,基于线程封装,由语言层调度,不是操作系统调度。

区别:

  1. 线程由 OS 调度、开销大;协程开销极小、可开上万个。
  2. 线程是抢占式;协程是协作式,主动挂起、恢复。
  3. 协程可非阻塞挂起,不阻塞当前线程。
  4. 协程自带上下文、作用域、生命周期,更适合 Android 异步开发。

2、协程的优点

答案

  1. 轻量、占用资源少,支持大量并发;
  2. 用同步写法写异步代码,告别嵌套回调;
  3. 自带作用域,可统一取消、防泄漏
  4. 灵活切换调度线程(主线程 / 子线程);
  5. 配合 Flow、Jetpack 生态适配更好。

3、什么是挂起函数 suspend?作用是什么

答案

suspend 修饰的函数是挂起函数

特点:

  1. 只能在协程或其他挂起函数中调用;
  2. 不会阻塞线程,只是挂起当前协程,让出线程执行其他任务;
  3. 耗时结束后自动恢复继续执行。

4、suspend 工作原理

答案

suspend 函数编译后携带续体对象,

执行时保存运行状态实现挂起,不阻塞线程;

异步任务完成后通过续体恢复现场继续执行,从而实现同步写法、异步执行。

加分短句

挂起阻塞协程,不阻塞线程;靠续体完成状态保存与恢复,无回调嵌套。

5、协程三大核心:协程作用域、调度器、生命周期

答案

  • 作用域 Scope:管理协程生命周期,统一取消、控制启停;
  • 调度器 Dispatcher:指定协程跑在哪个线程;
  • Job:协程任务句柄,可取消、监听完成状态。

6、四大调度器 Dispatcher 区别

答案

  • Dispatcher.Main:主线程,更新 UI;
  • Dispatcher.IO:子线程,适合网络、文件、数据库 IO;
  • Dispatcher.Default:子线程,适合CPU 密集型复杂计算;
  • Dispatcher.Unconfined:不指定线程,随上下文执行,几乎不用。

7、常用协程作用域有哪些

答案

方式用途推荐等级
GlobalScope全局协程(内存泄漏)❌ 不推荐
runBlocking测试 / 阻塞主线程⚠️ 仅测试
lifecycleScopeAndroid 页面异步任务✅ 顶级推荐
viewModelScopeViewModel 异步任务✅ 顶级推荐
CoroutineScope()自定义生命周期协程✅ 通用推荐
launch无返回值异步任务✅ 最常用
async + await并发任务 + 获取返回值✅ 常用
withContext线程切换(IO / 主线程)✅ 必用

8、为什么不推荐用 GlobalScope

答案

GlobalScope 生命周期贯穿整个 App,页面退出无法自动取消;里面的网络、延时任务还在后台跑,极易造成内存泄漏、浪费资源

9、launch 和 async 区别

答案

  • launch:无返回值,返回 Job,只管执行

  • async:有返回值,返回 Deferred,通过 await() 拿结果,适合并发任务

10、await () 作用

答案

等待 async 协程执行完成,挂起当前协程,拿到返回结果;不会阻塞线程,只是协程挂起。

11、协程怎么取消?取消原理

答案

通过 Job.cancel () 取消协程;

协程在挂起点会响应取消,抛出 CancellationException;作用域取消后,内部所有子协程全部级联取消

12、协程异常怎么处理?CoroutineExceptionHandler

答案

  1. 单个协程内部用 try-catch 包裹挂起函数;
  2. 全局统一异常用 CoroutineExceptionHandler
  3. launch 默认异常会崩溃;async 异常在 await 时才抛出。

13、协程结构化并发

结构化并发:子协程依附父协程作用域,父子生命周期绑定,自动取消、自动异常传递,杜绝协程泄漏。

核心三点

  1. 父子协程关系协程有父子层级,子协程继承父Scope、Dispatcher、异常策略
  2. 结构化取消父作用域一取消,所有子协程跟着全部级联取消,不用手动一个个解绑,天然防内存泄漏。
  3. 异常自动传播子协程抛异常,会向上传给父协程,统一捕获处理,不会静默崩溃、丢异常。

14、协程的启动模式几种

CoroutineStart 一共四种:DEFAULT、LAZY、ATOMIC、UNDISPATCHED

1、DEFAULT(默认)

立刻根据调度器调度执行,排队进入对应线程池,标准常规启动。

2、LAZY(懒启动)

不会自动执行,需要手动调用 start() / join() 才开始跑;适合需要延迟触发、手动控制时机的场景。

3、ATOMIC

立即在当前线程原子式启动,不可取消;在挂起之前,无法被取消,执行到第一个挂起点才响应取消。

4、UNDISPATCHED

直接在当前线程立刻执行,不切调度器;直到遇到第一个 suspend 挂起点,之后才按 Dispatcher 调度切换线程。

15、 let、also、with、run、apply 区别

函数名使用方法返回值内部持有对象作用
applyinfo.apply{this}本身this对象初始化、赋值属性
alsoinfo.also{it}本身it额外操作、日志、副作用
letinfo.let{it}最后一行it空安全调用、对象转换
runinfo.run{this}最后一行this计算、逻辑处理、返回结果
withwith(info) {this}最后一行this对一个对象做多次操作
1. let
  • 作用:空安全调用、对象转换
  • 关键字it(指代当前对象)
  • 返回:最后一行
  • 场景?.let 判空处理、数据转换
2. also
  • 作用:额外操作、日志、副作用
  • 关键字it
  • 返回自身对象
  • 场景:链式调用中加日志、初始化后处理
3. apply
  • 作用对象初始化、赋值属性
  • 关键字,直接用成员
  • 返回自身对象
  • 场景:View、Intent、Bean 赋值
4. run
  • 作用:计算、逻辑处理、返回结果
  • 关键字
  • 返回:最后一行
  • 场景:复杂计算、多步逻辑
5. with
  • 作用:对一个对象做多次操作
  • 关键字
  • 返回:最后一行
  • 场景:非空对象,简化调用

6.最核心记忆口诀(面试必背)

  • 初始化属性用 apply
  • 空安全处理用 let
  • 附加操作日志用 also
  • 计算返回值用 run
  • 非空简化调用用 with

7、记忆口诀:

  • “让它(let)转换,也(also)记录,运行(run)计算,应用(apply)配置,与(with)共事。”
  • 返回自身是 alsoapply (想记“AA”原则:Also Apply 都返回自己)。
  • 使用 it 的是 alsolet

16、Kotlin 协程怎么处理异常的?

deepseek_mermaid_20260121_981604.png

  1. 两种传播方式

    • launch 构建器:异常会立即在发生处抛出,并开始传播。
    • async 构建器:异常会延迟到您调用 .await() 获取结果时才抛出。
  2. 方式一:直接 try-catch(最基础)

直接在可能发生异常的代码块周围使用 try-catch。这对于 launchasync 都有效。

scope.launch {
    try {
        fetchData() // 挂起函数,可能抛出异常
    } catch (e: Exception) {
        // 处理异常,例如更新UI显示错误
        showError(e.message)
    }
}

// 对于 async,必须在 await() 处捕获
scope.launch {
    val deferred = async { fetchData() }
    try {
        val result = deferred.await()
        process(result)
    } catch (e: Exception) {
        // 处理来自 async 块的异常
    }
}

3. 方式二:CoroutineExceptionHandler(全局捕获)

这是一个上下文元素,用于在协程的根节点(通常是顶级或 SupervisorJob 下的直接子协程)捕获未处理的异常。它就像是协程世界的“全局 Thread.uncaughtExceptionHandler”。

// 1. 定义异常处理器
val handler = CoroutineExceptionHandler { _, exception ->
    println("协程上下文捕获到未处理异常: $exception")
    // 可以在这里上报崩溃日志
}

// 2. 在作用域中使用(注意:必须在根协程或 SupervisorJob 的直接子协程才有效)
val scope = CoroutineScope(Job() + handler) // 作为根作用域的一部分
scope.launch {
    // 如果这里发生异常,会被 handler 捕获
    throw RuntimeException("测试异常")
}

// 以下方式无效!异常会直接崩溃,因为handler不是根协程的
scope.launch {
    launch(handler) { // ❌ 错误:handler 不是这个 launch 的根
        throw RuntimeException("这个异常 handler 抓不到!")
    }
}

4. 方式三:SupervisorJob / supervisorScope(隔离异常)

这是防止“一个失败,全家遭殃” 的关键。默认情况下,父协程使用普通 Job,一个子协程失败会取消所有兄弟协程。SupervisorJob 改变了这一规则,使子协程的失败彼此隔离

// 1. 使用 SupervisorJob 创建作用域
val supervisorScope = CoroutineScope(SupervisorJob())
supervisorScope.launch {
    delay(1000)
    throw RuntimeException("子协程1失败") // 这个会崩溃或由handler处理
}
supervisorScope.launch {
    delay(2000)
    println("子协程2仍然会执行!") // 尽管协程1失败了,但协程2不受影响
}

// 2. 使用 supervisorScope 构建器(更常用)
suspend fun handleMultipleTasks() = supervisorScope { // 进入一个监督作用域
    val child1 = launch {
        throw RuntimeException("任务A失败")
    }
    val child2 = launch {
        delay(500)
        println("任务B正常完成") // 任务A的失败不会取消任务B
    }
    // 可以等待所有子协程,并单独处理它们的失败
    child1.join()
    child2.join()
}

5. 方式四:在 async 块内部捕获并返回结果状态

这是处理 async 异常的推荐模式,可以避免在 await() 时抛出异常,而是返回一个包含成功/失败信息的结果类。

// 定义结果密封类
sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Throwable) : Result<Nothing>()
}

suspend fun fetchDataSafely(): Result<Data> = withContext(Dispatchers.IO) {
    return@withContext try {
        Result.Success(apiService.getData()) // 假设 getData() 是 suspend fun
    } catch (e: Exception) {
        Result.Error(e)
    }
}

// 在调用处,无需 try-catch await()
scope.launch {
    val deferred = async { fetchDataSafely() }
    when (val result = deferred.await()) { // 这里不会抛出异常
        is Result.Success -> updateUI(result.data)
        is Result.Error -> showError(result.exception)
    }
}

17、launch为什么不用 join 也会执行?

launch 启动的协程,不用调用 join () 也会自动执行!

因为:

  • launch = 启动一个新的协程并自动执行
  • join () = 等待这个协程执行完

它们的关系是:

  • launch 负责 “启动并跑”
  • join 负责 “等它跑完”
val job = launch {
    delay(1000)
    println("任务完成")
}

job.join() // 等待协程执行完
println("全部结束")

结果:

//不加job.join()
全部结束 
任务完成
//加job.join()
任务完成 
全部结束
最核心总结
  • launch 会自动执行协程,不需要 join

  • join () 只做一件事:等待协程执行完毕,会阻塞尽量不要使用

  • 不加 join → 不等待,继续往下跑

  • 加了 join → 停下来等协程跑完再继续

一句话满分背诵版

launch 启动协程后会自动执行,join () 不是启动协程的,只是用来等待协程结束。

18、Kotlin 协程:到底是怎么 “挂载 / 切换” 到别的线程的?

一句话结论

协程本身不创建线程,它是靠 Dispatcher(调度器)协程体(你写的代码) 提交给线程池 / 主线程去执行,从而实现 “挂载到别的线程”。

你可以把协程理解成:一段可以被挂起、可以在线程之间搬运的代码块

1. 关键角色:Dispatcher(调度器)

它就是协程的 “线程司机”,负责决定:

  • 这段协程代码去哪个线程执行
  • 线程从哪里来(线程池)

Kotlin 自带 4 个常用调度器:

  1. Dispatchers.Main → 安卓主线程(UI 线程)
  2. Dispatchers.IO → IO 线程池(网络、数据库)
  3. Dispatchers.Default → CPU 密集型线程池
  4. Dispatchers.Unconfined → 不指定线程(几乎不用)

2. 协程 “挂载到别的线程” 的完整流程

我用最常用的 withContext 给你走一遍流程,一看就懂:

kotlin

scope.launch(Dispatchers.Main) {
    // 1. 现在:运行在 主线程

    // 2. 执行 withContext,协程挂起 ↓
    val result = withContext(Dispatchers.IO) {
        // 3. 现在:协程被“挂载”到 IO 线程池的某个线程
        api.request() // 耗时操作
    }

    // 4. 挂起恢复,自动切回 主线程
    updateUI(result)
}
3.底层发生了什么?(超清晰)
  1. 当前线程执行到 withContext协程发现要切换调度器,于是挂起不阻塞当前线程

  2. Dispatcher 把协程体提交给目标线程池Dispatchers.IO 内部是一个高效线程池,它会:

    • 从池里拿一条空闲线程
    • 把你写的 { ... } 代码块丢给这条线程执行
  3. 协程在新线程上恢复运行代码块运行时,当前线程已经变了,这就是 “挂载到别的线程”。

  4. 执行完,自动切回原来的线程协程有状态记录,执行完自动回到之前的调度器(主线程)。

4. 最容易踩的坑
  • 不指定 Dispatcher 默认继承外层协程的线程
  • 不要在主线程做耗时操作
  • Dispatchers.Main 必须添加依赖(Android)

5. 终极总结
  1. 协程本身不创建线程,靠 Dispatcher 切换线程
  2. 挂载 = 把协程代码块提交到目标线程池执行
  3. withContext 是最标准、最安全的线程切换方式

19、所有切换线程的方式

协程切换线程只有 2 种正确用法:

① launch /async 直接指定(启动就去目标线程)

kotlin

scope.launch(Dispatchers.IO) {
    // 一启动就在 IO 线程
}

② withContext 中途切换(最常用、官方推荐)

kotlin

withContext(Dispatchers.IO) {
    // 切换到 IO 执行
}

20、 withContext 作用?和 async/await 区别?

withContext切换调度器、切换线程,挂起函数,直接返回结果(最后一行的结果); 不需要手动 await,代码更简洁,推荐优先用

区别:

  • withContext 是串行切换(会阻塞下面的执行);
  • async 适合并发多任务

21、withContext 内部是串行还是并发?

一句话结论

withContext 内部代码块是串行执行,不是并发;它只是切换线程 / 调度器,不开启新协程。

详细解释(面试可直接说)
  1. withContext 只是一个挂起函数,作用是切调度器、切线程不会新建协程
  2. 大括号里的代码,从上到下顺序串行执行,一行走完才走下一行,没有并发。
  3. 如果想并发,必须手动用 async + await 开启多个子协程。
代码直观对比

1)withContext 串行(顺序执行)

kotlin

lifecycleScope.launch {
    println("开始")
    withContext(Dispatchers.IO) {
        delay(3000)
        println("withContext 内部")
    }
    println("必须等withContext完才走这里")
}

withContext 不新建协程(同一个协程内是串行),必须等

2)async 才是并发

kotlin

launch {
    val d1 = async(Dispatchers.IO) { task1() }
    val d2 = async(Dispatchers.IO) { task2() }
    // 两个任务同时并发跑
    d1.await()
    d2.await()
}

3)launch 也是并发

launch {} 一定会创建一个全新的子协程

1、核心结论

  • launch直接创建新协程,异步启动,不会阻塞 / 等待当前协程后续代码。
  • withContext不创建新协程,复用当前协程,只是切线程、挂起等待,后面代码要等它跑完。

2、代码对比一眼看懂

示例 1:launch 新建协程,不等待

kotlin

lifecycleScope.launch {
    println("外层开始")

    // 新开一个独立协程
    launch(Dispatchers.IO) {
        delay(3000)
        println("内层 launch 执行完")
    }

    // 不会等上面 launch,直接立马执行
    println("外层直接往下走,不等内层")
}

输出顺序:

plaintext

外层开始
外层直接往下走,不等内层
(3秒后)内层 launch 执行完

底层原理(面试加分)
  • withContext 只会切换当前协程的调度环境,始终还是同一个协程,单协程里代码必然串行。
  • async创建新的子协程,多个协程交给线程池调度,才能实现并发。
面试精准口述版
  1. launch 一定会创建新的子协程,属于协程构建器;
  2. 新协程独立运行,不会阻塞当前父协程,后面代码立刻执行,不用等待;
  3. 而 withContext 不创建新协程,复用当前协程,只是切换调度器并挂起,后续代码必须等待它执行完毕。

22、withContext 的后面代码也会等待withContext 执行后,再执行

withContext 是挂起函数后面的代码必须等 withContext 整块执行完、返回结果,才会往下走

1. 核心本质

withContextsuspend 挂起函数

  • 执行到 withContext协程挂起
  • 内部代码全部跑完 → 恢复协程
  • 再执行 withContext 后面的代码

2. 示例一看就懂

kotlin

lifecycleScope.launch {
    println("1 开始")

    // 会卡在这里等内部全部执行完
    val res = withContext(Dispatchers.IO) {
        delay(2000) // 模拟耗时
        println("2 耗时任务结束")
        "结果"
    }

    // 必须等上面 withContext 完事,才会走到这里
    println("3 执行后面代码:$res")
}

执行顺序:1 开始 → 等 2 秒 → 2 耗时任务结束3 执行后面代码

3. 为什么会等待?

  1. withContext 不是开新协程,还是当前同一个协程
  2. 挂起函数的特性:调用处原地等待完成,不阻塞线程,但阻塞当前协程流程
  3. 天然是串行等待的效果

4. 对比:谁会等、谁不会等

  • withContext:后面代码等待,串行
  • delay:后面代码等待,串行
  • launch 嵌套:不会等,直接往下走,并发

kotlin

launch {
    launch(Dispatchers.IO) {
        delay(2000)
        println("子协程")
    }
    println("立马执行,不等上面")
}

面试一句话背记

withContext 是挂起函数,会阻塞当前协程流程,后面代码必须等它执行完毕才会继续执行;但它不会阻塞线程。

22、launch 内部是串行还是并发执行?

面试标准答案:launch 内部代码是串行,整体是并发

一句话秒杀

- launch 内部代码:从上到下 串行执行。

- launch 和外部 / 其他 launch:是 并发执行。

1. launch 内部的代码 → 串行

launch {} 大括号里面的代码,永远是顺序执行、一行一行跑,没有并发。

kotlin

scope.launch {
    // 内部:串行!顺序执行!
    task1()
    task2() // 必须等 task1 完才执行
    task3() // 必须等 task2 完才执行
}

原因:一个协程就是一个执行流,单执行流一定是串行的


2. launch 和外部代码 → 并发

launch创建新协程,新协程和外部协程同时跑

kotlin

scope.launch {
    launch {
        delay(1000)
        println("A") // 并发执行
    }

    println("B")     // 不会等上面,直接打印
}

输出顺序:

plaintext

B1秒后)A

原因多个协程之间是并发的。


3. 多个 launch 之间 → 并发

kotlin

scope.launch {
    launch { taskA() } // 并发
    launch { taskB() } // 并发
    launch { taskC() } // 并发
}

A、B、C 同时跑,谁先跑完不一定。


面试满分回答(背这段)
  1. launch 内部代码是串行执行,从上到下顺序执行;
  2. launch 会创建新协程,新协程与外部协程、以及其他 launch 之间是并发执行
  3. 它不会等待内部执行完,外部代码会继续往下走。
终极对比(面试必考对比)

表格

方式是否创建协程内部执行后面代码是否等待
launch✅ 新建串行不等待(并发)
withContext❌ 不新建串行等待(串行)
async✅ 新建串行不等待(并发)

23、 lifecycleScope /viewModelScope 原理?

  • viewModelScope:绑定 ViewModel 生命周期,ViewModel 销毁自动取消协程
  • lifecycleScope:绑定 Activity/Fragment 生命周期,页面销毁自动 cancel内部通过监听生命周期回调,自动取消协程,防泄漏

24、 CoroutineExceptionHandler 什么时候生效?

只对 根协程 生效,子协程异常会向上抛给根协程,由全局异常处理器捕获。

25、Flow 和 普通协程区别?

: 协程单次返回一个结果(async+await);

Flow 是冷数据流,可以连续发送多个值,类似观察者模式,适合分页、实时数据、倒计时、接口轮询。

26、 协程挂起底层原理?

Kotlin 编译器把 suspend 函数编译成状态机,拆分多个状态节点,挂起时保存上下文,恢复时从对应状态继续执行,不用阻塞线程。

27、runBlocking 用处?业务能用吗?

runBlocking 会阻塞当前线程

只用于单元测试、main 函数调试,业务代码禁止使用

28、 协程会内存泄漏的场景有哪些?

  1. 滥用 GlobalScope
  2. 协程内部持有 Activity/Fragment 引用
  3. 自定义 Scope 没手动 cancel
  4. 异步任务没取消,页面销毁还在回调

29、自定义协程(标准步骤)

  1. 组合 Dispatchers 调度器 + Job/SupervisorJob
  2. 手动构造 CoroutineScope
  3. launch/async 在自定义作用域里开启协程
  4. 页面 / 业务销毁时 手动调用 cancel () 取消全部协程
// 自定义作用域
private val myScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

// 开启自定义协程
fun requestData() {
    myScope.launch {
        // 耗时任务、网络请求
    }
}

// 销毁时必须取消,防泄漏
fun destroy() {
    myScope.cancel()
}

自定义注意事项(必背)

  1. 严禁用 GlobalScope 替代自定义
  2. 必须自己手动 cancel,否则内存泄漏
  3. 优先用 SupervisorJob 做异常隔离
  4. 自定义线程池一定要有界队列,防止 OOM
  5. 自定义 Dispatcher 用完要 close
  6. 作用域要和业务生命周期绑定

30、Kotlin协程为什么能以同步代码写出异步逻辑?

一句话核心

依靠挂起函数 + 状态机切线程,代码写法同步,底层执行异步。

精简完整版
  1. Kotlin 协程通过 suspend 挂起函数实现代码暂停与恢复,不会阻塞主线程
  2. 编译期把挂起代码拆分成多个状态片段,自动生成状态机。
  3. 遇到耗时挂起函数自动切子线程执行,执行完毕自动切回原线程
  4. 开发者只用顺序写同步风格代码,线程切换、回调拆分全部由编译器自动完成,摆脱回调地狱。
超短背诵版

协程利用挂起函数暂停任务,编译生成状态机自动分片执行,自动完成线程切换,上层写同步顺序代码,底层异步执行,所以能同步写法实现异步逻辑。

31、在协程上定义一个局部变量,为什么再其他线程里的协程也能访问到?

精简原理
  1. Kotlin 协程本质还是代码片段,局部变量会被闭包捕获,提升为类成员变量,不再是临时栈变量。
  2. 同一作用域下所有子协程,共用同一个作用域对象,变量引用完全一致。
  3. 协程只是切换线程执行代码,变量引用没变,所以不管切到哪个线程,都能读到同一个变量。
  4. 区别:只是执行线程变了,内存变量地址不变
通俗大白话

你在父协程定义局部变量,协程编译时自动把这个变量包进闭包,所有子协程不管跑在主线程还是子线程,用的都是同一个内存地址变量,所以全都能访问。

面试加分提醒
  1. 能访问不等于线程安全,多协程同时修改会出现并发问题。
  2. 基本类型、引用类型都会被闭包持有,跨线程协程互通。

32、闭包什么意思?

一句话记住

函数内部,能拿到外面函数的局部变量,这个函数 + 外面的变量 = 闭包

大白话
  1. 外面方法定义了局部变量
  2. 里面的 Lambda / 匿名函数,抓走这个变量一直持有
  3. 哪怕外层代码走完销毁了,内部还能继续用这个变量这就叫闭包
协程场景最直白例子

kotlin

fun main() {
    // 外层局部变量
    var num = 0

    // 协程就是内部函数,抓走了 num
    GlobalScope.launch {
        delay(1000)
        num++ // 隔一秒还能访问外面的局部变量
        println(num)
    }
}

本来方法走完局部变量就销毁,被协程抓走持有了,一直活着,这就是闭包。

3 个核心特点(面试背)
  1. 内部函数访问外部局部变量
  2. 延长局部变量生命周期
  3. 多个内部代码共享同一个变量
最短背诵

闭包就是内层代码捕获外层局部变量,持有引用,延长变量生命周期,跨作用域依然能访问使用。

33、什么是Kotlin协程的状态机?

编译器把 suspend 挂起代码,拆分成多段代码片段,靠状态标记分段执行,这就是协程状态机。

  1. 你写同步顺序代码

kotlin

suspend fun test(){
    println("1")
    delay(1000)
    println("2")
}

2. 编译后自动切成两段

  • 状态 0:执行打印 1,遇到 delay暂停
  • 状态 1:延迟结束,恢复执行打印 2
  1. 数字状态值记录执行到哪一步,暂停存状态,恢复按状态继续跑,就是状态机
核心特点
  1. 不用手写回调,编译器自动拆分
  2. 挂起时保存当前状态,让出线程
  3. 恢复时读取状态,从断点继续执行
  4. 本质:分段执行 + 状态记录
面试必背短句

协程状态机是 Kotlin 编译器生成的,将连续挂起代码拆分成多个执行节点,通过状态值记录运行位置,实现暂停与恢复,从而做到同步写法异步执行。

34、在单个协程内部,delay是串行的,必须等delay之后,再执行后面的代码?

一句话

同一个协程内部,代码自上而下串行执行,delay 会挂起当前协程,必须等延时结束,才会往下走后面代码。

代码直观证明

kotlin

runBlocking {
    println("开始")
    delay(1000) // 挂起,等1秒
    println("执行完第一个延迟")
    delay(2000) // 再等2秒
    println("全部结束")
}

执行顺序:开始 → 等 1 秒 → 打印 → 再等 2 秒 → 最后打印严格排队串行,不能并行

核心区分
  1. 同一协程内:delay 串行排队,依次等待执行
  2. 不同协程:互不影响,并发执行,互不等待
最简背诵

单个协程里代码顺序执行,delay 暂停当前协程,延时结束才执行下行代码,属于串行执行。