Appearance
运行时承载模型入口
模块位置:并发与运行时底座 README · 这是 01 模块里专门用来串起“执行现场”心智的入口页 相关主题:Java 并发模型总览 · Kotlin 协程挂起恢复与并发 · Dart Future / Event Loop / Isolate · JVM / Dart 内存模型
一句话定位
这页不展开 Java / Kotlin / Dart 的专题细节,而是先回答一个总问题:代码到底被谁承载、暂停时现场放哪、恢复时靠什么继续跑。把这个入口建立好,后面的线程、协程、Continuation、isolate、event loop、栈/堆/GC 才不会散成碎片。
为什么要先看这页
01 模块原来已经有 Java / Kotlin / Dart 各自主线,但缺少一个显式入口,把下面这些问题放到同一张图里:
下面每个问题紧跟答案,结合真实代码机制与生动比喻,帮你在同一张图里建立"执行现场承载"心智。
先建立一张最小地图
可以先把运行时承载模型粗分成四层:
| 层级 | 关注点 | Java / JVM | Kotlin 协程 | Dart |
|---|---|---|---|---|
| 执行载体 | 到底是谁在跑代码 | OS 线程 / 线程池线程 | 仍借 JVM 线程执行 | isolate 内的事件循环 |
| 执行现场 | 代码跑到哪、局部变量放哪 | 线程栈帧 | 挂起前一部分仍在线程栈;挂起后关键状态转成 Continuation / 状态机对象 | 当前调用栈 + await 后续 continuation |
| 共享数据承载 | 对象和共享状态放哪 | 共享堆 | 共享堆(仍受 JVM 内存模型约束) | 每个 isolate 自己的堆 |
| 恢复 / 调度机制 | 暂停后怎么继续 | 线程继续执行 / 调度器唤醒 | Dispatcher 选线程,Continuation 恢复 | event loop 重新排队并恢复 |
先记一句:
- Java 重点是“线程 + 共享堆”
- Kotlin 协程 重点是“线程之上的挂起状态机”
- Dart 重点是“isolate + event loop + 独立堆”
Q1. Java 线程跑代码时,执行现场在什么地方?
答:在**「线程栈」的栈帧(stack frames)** 里。每调用一个方法就压入一个栈帧,帧中装三样东西:局部变量表、操作数栈 / 返回地址(执行到哪一步、做完回调用者哪一行)、对堆对象的引用(注意:只是引用,对象本身不在栈上)。
java
void a() { int x = 1; b(); } // a 的栈帧:x=1,返回点 = a 里 b() 之后
void b() { int y = 2; c(); } // b 的栈帧:y=2
void c() { /* 此刻调用栈:[a帧, b帧, c帧] */ }跑进 c() 时,调用栈是 [a, b, c],x、y 各自锁在自己的帧里,线程私有、互不可见。而 new User() 出的对象在堆(heap) ——堆是线程共享的;指向它的引用 User u 才在栈帧里。
比喻:调用栈像一摞盘子,每调一个方法压一个盘子,盘子上写"局部变量 + 做完回哪步"。a 的盘子压着 b 的盘子压着 c 的盘子,从顶往下看就是当前执行现场。
Q2. Kotlin suspend 挂起后,局部变量和"下一步从哪继续"由谁保存?
答:由**编译器改写出的「状态机对象 + Continuation」**保存,不是靠一直占着线程栈。Kotlin 编译器对 suspend 函数做 CPS(Continuation-Passing Style)变换:把它变成一个带 label 字段的状态机,实现 kotlin.coroutines.Continuation 接口。挂起时:
- 局部变量被提升为状态机对象的字段(逃出栈,活到堆)
label记下"停在第几个挂起点"- 这个 continuation 被交给挂起函数(如
delay/suspendCoroutine),由它未来调resumeWith()恢复
kotlin
suspend fun loadUser(): User {
val raw = api.fetchUser() // 挂起点
return parse(raw)
}编译后大致等价于:
kotlin
class LoadUserSM(cont: Continuation) : ContinuationImpl {
var label = 0
var raw: RawData? = null // 局部变量 raw 提升为字段
fun invokeSuspend(result: Any?): Any? {
return when (label) {
0 -> { label = 1; api.fetchUser(this) } // 把 this 传进去 → 挂起,线程栈帧弹出
1 -> { raw = result as RawData; parse(raw) } // 恢复:raw 还在字段里
}
}
}挂起那一刻,loadUser 的栈帧消失了(线程不用为它空等),但 LoadUserSM 对象在堆里活着。等 fetchUser 完成,Dispatcher 挑一条线程(可能和原来不是同一条)调 resumeWith,状态机从 label = 1 续上,raw 还在,parse 照常拿。
对比 Java:Java 线程"挂起"= 线程阻塞等待,栈帧一直占着不释放(OS 线程切换贵);Kotlin 协程"挂起"= 栈帧可释放、状态在堆里,所以一个线程能跑成千上万个协程。
比喻:看书看到第 50 页要停,你在书签上写"看到第 50 页、第三章、手边红笔在第三页"。书合上(栈帧回收),但书签(Continuation)带着进度和手边物(局部变量)留在桌上(堆)。下次回来(可能换了座位 = 换线程),凭书签继续读。
Q3. Dart await 之后,为什么不是"切线程回来",而是事件循环恢复 continuation?
答:因为 Dart 默认是**「单 isolate 单线程事件循环」**,没有"另一个线程把结果送回来"这回事。一个 async 函数遇到 await future 时:
- 函数在此暂停,把"后半段代码"打包成 continuation 交出去
- 当前 isolate 的调用栈继续往下走(不含这个 async 的后半段)
- 等
future完成,事件循环把 continuation 重新排队 - 轮到它时,仍在同一个 isolate、同一条线程恢复后半段
不存在线程切换,是因为同一 isolate 内所有 Dart 代码顺序、非并行执行(优先级:sync > microtask > event queue)。
dart
void main() {
print('A');
fetchData(); // async,main 不等它
print('B');
}
Future<void> fetchData() async {
print('C');
await Future(() => print('D')); // 挂起,print('E') 作为 continuation 排队
print('E');
}输出顺序:A → C → B → D → E。注意 E 在 B 之后,且 D、E 都跑在主 isolate 同一条线程上,没有新线程。
工程坑:很多人以为"包了 Future 就不会卡 UI"——错。Future(() { 大JSON解析 }) 只是把任务丢进当前 isolate 的 event queue,还是在主 isolate 跑,照样掉帧。CPU 重活得迁到 Isolate.run / compute。
比喻:你在排队办业务(事件循环),轮到你交表(同步代码),办事员说"先去旁边等,好了叫你"(await 挂起,你离开柜台 = 栈弹出后半段)。你没去另一个窗口(没切线程),就在大厅等候区(event queue)排队。叫号(future 完成、continuation 重入队)后,你回同一柜台同一办事员(同 isolate 线程)继续办剩下的(print E)。
Q4. isolate 为什么既像并发单元,又和 JVM 线程不是一回事?
答:像并发单元——多个 isolate 各自跑在各自 OS 线程上,可多核真正并行,和 Java 线程一样是"能并行的工作单元"。不像 JVM 线程,根本差别在内存模型:
| JVM 线程 | Dart isolate | |
|---|---|---|
| 内存 | 共享同一进程地址空间 + 共享堆 | 自带独立堆,不共享对象 |
| 线程间通信 | 共享内存 + 锁 / 原子 / volatile | 消息传递(SendPort / ReceivePort),传可序列化副本 |
| 竞态防护 | 靠锁 + JMM | 无共享可变状态 → 天然无数据竞争,不需要锁 |
所以:线程 = 共享内存,靠锁防止两人同时改白板;isolate = 内存隔离,靠消息传递,没有共享可变状态,synchronized / volatile 那一套默认用不上。
比喻:Java 线程 = 同一间办公室多个员工共享一块白板(堆),谁都能写,所以要定规矩(加锁)防把字擦掉;isolate = 多个独立小隔间,各有自己的白板、彼此看不见,协作只能写纸条从门缝递(消息传递),纸条是抄写下来的副本(序列化),不是把白板搬过去。
Q5. 栈、堆、Continuation、event loop、isolate 这些词各自属于哪一层?
答:用本模块入口页的四层地图归类:
| 概念 | 归属层 | 属于哪个运行时 | 作用 |
|---|---|---|---|
| 栈 / 调用栈 | 执行现场层 | OS 线程 / JVM 线程 / isolate 调用栈 | 方法调用链、局部变量、返回点 |
| 堆 | 数据承载层 | JVM 共享堆 / isolate 各自独立堆 | 对象实例、数组、共享状态 |
| Continuation | 执行现场层(挂起态) | Kotlin 协程 / Dart await | 保存"进度 + 局部变量",供恢复 |
| event loop | 恢复 / 调度机制层 | Dart isolate | 按 microtask / event 队列调度、恢复 continuation |
| isolate | 执行载体层(独立单元) | Dart | 自带事件循环 + 独立堆的并发隔离单元 |
从底层到高层的完整分层:
- 执行载体(谁在跑):OS 线程(Java 借的)/ isolate(Dart 自带)
- 执行现场(现场在哪):线程栈帧(Java 同步)/ Continuation 对象(Kotlin·Dart 挂起后状态在堆)
- 数据承载(对象在哪):共享堆(Java)/ 每 isolate 独立堆(Dart)
- 恢复 / 调度(怎么续):调度器唤醒 / Dispatcher(Java·Kotlin)/ event loop(Dart)
把三语言串一遍:
- Java:线程 → 栈帧 → 共享堆 → 调度器唤醒
- Kotlin:线程(借)→ Continuation → 共享堆 → Dispatcher
- Dart:isolate → 调用栈 + await continuation → 独立堆 → event loop
如果这个入口缺失,学习时就容易出现两种常见断裂:
- 只会按语言背 API,不知道它们都在解决“执行现场承载”这个共同问题。
- 把本该在 01 模块建立的运行时心智,误丢到 02 模块的内存/生命周期里再回头拼图。
三条主线各自回答什么
1. Java:线程栈 + 共享堆
在 JVM 里,最经典的现场拆法是:
- 线程栈:方法调用链、局部变量、当前执行位置
- 堆:对象实例、数组、共享状态
所以 Java 并发的第一性问题不是“怎么异步”,而是:
- 哪些数据在线程自己的栈里,天然私有
- 哪些对象进了共享堆,会被多线程同时读写
- 共享堆上的修改如何通过 JMM、锁、
volatile、原子类变得可见且安全
继续展开看:Java 并发模型总览
2. Kotlin:Continuation 承载挂起后的执行现场
Kotlin 协程不是新线程,而是把:
- 挂起点前后的执行阶段
- 局部变量
- 下次恢复的位置
改写成 状态机 + Continuation。
这意味着:
- 协程运行时仍借 JVM 线程
- 但协程一旦挂起,关键现场不必一直躺在线程栈上等着
- 恢复时由 Dispatcher 决定回到哪类线程继续跑
理解这一点后,suspend、withContext、launch、async、取消传播才会落位。
继续展开看:Kotlin 协程并发总览
3. Dart:isolate 承载事件循环与独立堆
Dart 的默认心智不是“多个线程共享一个堆”,而是:
- 一个 isolate 有自己的事件循环
- 一个 isolate 有自己的调用栈和堆
Future/await默认只是这个 isolate 内部的调度语义- 真正要隔离 CPU 重活,靠的是 isolate,而不是普通
Future
所以 Dart 里的关键不是“有没有异步”,而是:
- 当前任务是不是还卡在主 isolate
await后续何时被 event loop 恢复- 这个任务是否已经重到该迁到新 isolate
继续展开看:Dart 并发总览:Future / event loop / isolate
为什么这页要同时连到 02 模块
01 模块关心“执行现场如何被承载与恢复”,02 模块关心“对象活在哪里、为什么还没释放、为什么会 GC / 泄漏 / 卡顿”。
两者边界可以这样记:
- 01 模块:谁在跑、怎么挂起、怎么恢复、共享模型是什么
- 02 模块:对象在哪、引用链怎么活、何时回收、为何泄漏或抖动
如果你此刻对“栈 / 堆 / isolate 独立堆”还没完全稳住,可以接着看:JVM / Dart 内存模型
推荐进入顺序
- 先读本页,建立“执行载体 / 执行现场 / 数据承载 / 恢复机制”四层地图
- 再读 Java 并发模型总览,把线程栈、共享堆、JMM 放稳
- 再读 Kotlin 协程挂起恢复与并发,把 Continuation 放到 Java 线程模型之上理解
- 再读 Dart Future / Event Loop / Isolate,看 isolate 和 event loop 如何替代共享内存主模型
- 最后回到 JVM / Dart 内存模型,把执行现场和对象存活串起来
对应实验入口
| 主题 | Lab 说明 | 源码 |
|---|---|---|
| Kotlin 挂起恢复 / Dispatcher / Continuation | coroutine-execution-flow | CoroutineExecutionFlowDemo.kt |
Kotlin Mutex / StateFlow / SharedFlow | coroutine-mutex-stateflow | CoroutineMutexStateFlowDemo.kt |
Dart event loop / await / isolate | dart-event-loop-isolate | event_loop_isolate_demo.dart |
复习检查题
为什么说“线程 / 协程 / isolate”不是同一层概念?
答:因为线程是 OS / JVM 层执行载体,协程是运行在线程之上的可挂起任务模型,isolate 是 Dart 里的独立运行时单元,带自己的事件循环与堆。它们都和“执行”有关,但承载层级不同。
Kotlin 协程挂起后,为什么不能只拿“线程栈”解释全部执行现场?
答:因为挂起后协程的关键状态会被改写进状态机 / Continuation 对象,不再要求一直占着原线程栈等待;恢复时还能由 Dispatcher 安排到合适线程继续执行。
Dart 里为什么
Future不是 isolate,也不是线程?答:因为普通
Future默认只是当前 isolate 事件循环中的调度单元,只是延后或分段执行代码;它既不自动创建独立堆,也不自动获得新的并发隔离执行单元。为什么“执行现场入口”应该放在 01 模块,而不是完全并入 02 内存模块?
答:因为这里先回答的是“谁在跑、怎么挂起、怎么恢复、并发单元如何承载”,这是运行时与并发底座问题;02 模块再继续回答对象如何存活、回收、泄漏。两者相关,但不是同一个入口问题。
速记
- Java:线程栈承载当前执行,堆承载共享对象
- Kotlin:协程挂起后靠 Continuation 保存恢复现场
- Dart:isolate = 独立事件循环 + 独立堆
await/suspend的关键不是“换线程”,而是现场保存与恢复- 01 先建运行时承载模型,02 再看内存存活与生命周期
整页收口比喻(做菜模型)
栈 / 调用栈 = 厨师手边的操作台,每道工序摆一层;堆 = 公共冰箱(Java,谁都能开)/ 每个小厨房自己的冰箱(Dart isolate,不共享);线程 = 一个厨师(Java,多厨师共享冰箱,要排班防冲突);isolate = 独立小厨房,自带厨师 + 冰箱 + 传菜窗口(消息传递);Continuation = 厨师中途被打断写的便签:做到哪步、手边材料放哪、回来从哪继续;event loop = 小厨房叫号系统:先清手头同步活 → 再清空"优先小纸条"(microtask)→ 再处理"普通订单"(event queue),按号恢复半成品。
把这五个词放进同一张图:执行载体(线程 / isolate)→ 执行现场(栈帧 / Continuation)→ 数据承载(共享堆 / 独立堆)→ 恢复调度(调度器 / event loop),运行时心智就立住了。