Skip to content

运行时承载模型入口 ​

模块位置:并发与运行时底座 README · 这是 01 模块里专门用来串起“执行现场”心智的入口页 相关主题:Java 并发模型总览 · Kotlin 协程挂起恢复与并发 · Dart Future / Event Loop / Isolate · JVM / Dart 内存模型

一句话定位 ​

这页不展开 Java / Kotlin / Dart 的专题细节,而是先回答一个总问题:代码到底被谁承载、暂停时现场放哪、恢复时靠什么继续跑。把这个入口建立好,后面的线程、协程、Continuation、isolate、event loop、栈/堆/GC 才不会散成碎片。

为什么要先看这页 ​

01 模块原来已经有 Java / Kotlin / Dart 各自主线,但缺少一个显式入口,把下面这些问题放到同一张图里:

下面每个问题紧跟答案,结合真实代码机制与生动比喻,帮你在同一张图里建立"执行现场承载"心智。

先建立一张最小地图 ​

可以先把运行时承载模型粗分成四层:

层级关注点Java / JVMKotlin 协程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 时:

  1. 函数在此暂停,把"后半段代码"打包成 continuation 交出去
  2. 当前 isolate 的调用栈继续往下走(不含这个 async 的后半段)
  3. 等 future 完成,事件循环把 continuation 重新排队
  4. 轮到它时,仍在同一个 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自带事件循环 + 独立堆的并发隔离单元

从底层到高层的完整分层:

  1. 执行载体(谁在跑):OS 线程(Java 借的)/ isolate(Dart 自带)
  2. 执行现场(现场在哪):线程栈帧(Java 同步)/ Continuation 对象(Kotlin·Dart 挂起后状态在堆)
  3. 数据承载(对象在哪):共享堆(Java)/ 每 isolate 独立堆(Dart)
  4. 恢复 / 调度(怎么续):调度器唤醒 / Dispatcher(Java·Kotlin)/ event loop(Dart)

把三语言串一遍:

  • Java:线程 → 栈帧 → 共享堆 → 调度器唤醒
  • Kotlin:线程(借)→ Continuation → 共享堆 → Dispatcher
  • Dart:isolate → 调用栈 + await continuation → 独立堆 → event loop

如果这个入口缺失,学习时就容易出现两种常见断裂:

  1. 只会按语言背 API,不知道它们都在解决“执行现场承载”这个共同问题。
  2. 把本该在 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 内存模型

推荐进入顺序 ​

  1. 先读本页,建立“执行载体 / 执行现场 / 数据承载 / 恢复机制”四层地图
  2. 再读 Java 并发模型总览,把线程栈、共享堆、JMM 放稳
  3. 再读 Kotlin 协程挂起恢复与并发,把 Continuation 放到 Java 线程模型之上理解
  4. 再读 Dart Future / Event Loop / Isolate,看 isolate 和 event loop 如何替代共享内存主模型
  5. 最后回到 JVM / Dart 内存模型,把执行现场和对象存活串起来

对应实验入口 ​

主题Lab 说明源码
Kotlin 挂起恢复 / Dispatcher / Continuationcoroutine-execution-flowCoroutineExecutionFlowDemo.kt
Kotlin Mutex / StateFlow / SharedFlowcoroutine-mutex-stateflowCoroutineMutexStateFlowDemo.kt
Dart event loop / await / isolatedart-event-loop-isolateevent_loop_isolate_demo.dart

复习检查题 ​

  1. 为什么说“线程 / 协程 / isolate”不是同一层概念?

    答:因为线程是 OS / JVM 层执行载体,协程是运行在线程之上的可挂起任务模型,isolate 是 Dart 里的独立运行时单元,带自己的事件循环与堆。它们都和“执行”有关,但承载层级不同。

  2. Kotlin 协程挂起后,为什么不能只拿“线程栈”解释全部执行现场?

    答:因为挂起后协程的关键状态会被改写进状态机 / Continuation 对象,不再要求一直占着原线程栈等待;恢复时还能由 Dispatcher 安排到合适线程继续执行。

  3. Dart 里为什么 Future 不是 isolate,也不是线程?

    答:因为普通 Future 默认只是当前 isolate 事件循环中的调度单元,只是延后或分段执行代码;它既不自动创建独立堆,也不自动获得新的并发隔离执行单元。

  4. 为什么“执行现场入口”应该放在 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),运行时心智就立住了。

站点构建时间:2026/8/24 23:43:17