Appearance
跨语言运行时并发总对照:Java / Kotlin / Dart / JS / Go
所属模块:并发与运行时底座 README
建议前置:运行时承载模型入口 · Java 并发模型总览 · Kotlin 协程并发总览 · Dart 并发总览:Future / event loop / isolate · JS Event Loop / Promise / Worker · Go goroutine / channel / context
一句话定位
这页不是讲百科,而是给 Android 背景、正在补 Flutter/Dart 体系的工程师 一张跨语言判断表:当你看到 Thread、coroutine、Future、Promise、goroutine、isolate、Worker 时,先别问“语法怎么写”,先问 谁在承载执行、挂起后现场放哪、共享状态怎么处理、取消和背压怎么治理。
代码索引
| 语言主线 | 主题页 | 对应实验 |
|---|---|---|
| Java | Java 并发模型总览 | thread-pool-basics · volatile-vs-atomic-vs-lock |
| Kotlin | Kotlin 协程并发总览 | coroutine-execution-flow · coroutine-mutex-stateflow |
| Dart / Flutter | Dart 并发总览:Future / event loop / isolate | dart-event-loop-isolate |
| JS | JS Event Loop / Promise / Worker | 当前以理论文档为主,建议后续补 labs/node/runtime-concurrency/ |
| Go | Go goroutine / channel / context | 当前以理论文档为主,建议后续补 labs/go/runtime-concurrency/ |
为什么这页值得单独看
如果你从 Android 转到 Flutter / Dart,最常见的误判不是语法不会,而是把 Java/Kotlin 的共享内存直觉 直接套到 Dart / JS / Go:
- 把
Future当成“后台线程句柄” - 把协程当成“更轻线程”就结束
- 把 Promise 当成“不会卡 UI 的异步”
- 把 goroutine 当成“随便开、总会自己收”的廉价线程
- 把 isolate 想成“Dart 版协程”
这页要帮你建立的不是定义,而是以下工程判断:
- 当前问题是调度问题、共享状态问题,还是隔离问题?
- 我需要的是异步编排,还是需要真正把 CPU 重活挪出去?
- 当前模型主要靠共享内存,还是主要靠消息传递?
- 取消、超时、背压、生命周期治理在这个语言里靠什么落地?
推荐先看的五个对照维度
1. 执行载体:代码到底由谁承载
| 语言 | 默认执行载体 | 工程上怎么理解 |
|---|---|---|
| Java | OS 线程 / 线程池线程 | 任务最终一定落到真实线程上跑,线程数量、阻塞、切换成本都要管 |
| Kotlin | 仍借 JVM 线程,协程本身不是线程 | 协程是“任务描述 + 挂起恢复语义”,真正执行仍靠 Dispatcher 背后的线程 |
| Dart | 当前 isolate 的 event loop | 同一 isolate 内默认不并行;大部分 Future 只是排队,不是开新线程 |
| JS | 主线程 event loop | Promise/async 先理解为“回调何时恢复”,不是线程切换 |
| Go | goroutine,由 runtime 调度到少量 OS 线程 | 任务很轻,但不是免费;阻塞、泄漏、背压设计不好一样出事 |
工程结论
- Java / Kotlin 更像“多线程世界里如何安全组织任务”。
- Dart / JS 更像“单执行主线里如何安排回调,并在必要时显式隔离重活”。
- Go 是“轻量任务很多,但仍要对调度、退出和资源负责”。
2. 挂起与恢复:现场放哪、靠什么继续跑
| 语言 | 挂起/等待时现场主要放哪 | 恢复机制 |
|---|---|---|
| Java | 线程阻塞时现场仍主要在线程栈,Future 结果在堆对象里 | 线程被唤醒、继续执行,或线程池里其他线程取到下一个任务 |
| Kotlin | 编译器改写成状态机 + Continuation,关键局部状态转到堆对象 | Dispatcher 选择线程,Continuation 恢复后半段 |
| Dart | async / await 后续变成 continuation,排回当前 isolate 队列 | event loop 在 Future 完成后继续恢复 |
| JS | Promise.then / await 后续进入 microtask | event loop 清空 microtask 时恢复回调 |
| Go | goroutine 被 runtime 挂起/阻塞,栈可增长收缩 | runtime 调度 goroutine 继续执行 |
工程结论
- Kotlin 和 Dart/JS 都有“看起来顺序写,底层其实是恢复后半段”的特征,但 Kotlin 站在多线程调度器之上,Dart/JS 站在事件循环之上。
- Java 的
Future更像“结果句柄”;Kotlin/Dart/JS 的suspend/await更强调“把后半段恢复出来”。 - Go 不强调
await语法,而是靠 runtime 调度 goroutine + channel/context 组织等待与退出。
3. 共享状态模型:靠共享内存还是消息传递
| 语言 | 主模型 | 共享状态风险点 | 第一反应手段 |
|---|---|---|---|
| Java | 共享内存 | 可见性、竞态、锁竞争、错误发布 | synchronized / Lock / volatile / Atomic* / 并发容器 |
| Kotlin | 共享内存 + 协程编排 | 误以为“用了协程就线程安全” | Mutex、受控 Dispatcher、StateFlow、结构化并发 |
| Dart | isolate 隔离,不共享普通对象 | 主 isolate 被重活占住、消息复制成本 | isolate / Isolate.run / compute |
| JS | 主线程单共享执行线,Worker 走消息传递 | UI 长任务、回调顺序错觉 | Worker / 分块处理 / 限制 microtask 滥用 |
| Go | 倾向“通过通信共享,而不是通过共享来通信” | goroutine 泄漏、channel 阻塞、缓冲失控 | channel、context、明确退出协议 |
工程结论
- Java / Kotlin 的难点是 共享状态怎么证明安全。
- Dart / JS 的难点是 你以为自己异步了,其实重活还留在主执行线上。
- Go 的难点是 任务很多时如何停得干净、背压设计是否健康。
4. 真正并发 vs 只是异步
| 场景 | Java | Kotlin | Dart / Flutter | JS | Go |
|---|---|---|---|---|---|
| 发起一个异步 IO | 线程池任务 / 非阻塞框架回调 | suspend + withContext(IO) 等 | Future / await | Promise / await | goroutine + IO / netpoll |
| 让 UI/主执行线不被大计算卡住 | 把任务移到后台线程池 | withContext(Default) 或专门线程池 | isolate / compute | Worker | goroutine 通常已可并行,但仍要控制数量 |
| 只是把回调延后,不等于并行 | Future/回调句柄本身不等于并行 | 协程本身不等于独占线程 | Future(() {}) 不等于新 isolate | Promise 不等于新线程 | goroutine 常常真并发,但不等于无限扩张 |
工程结论
- 异步 ≠ 并行 这条在 Dart / JS 最容易踩坑,在 Kotlin 次之,在 Java/Go 容易因为线程/GMP 存在而被误以为天然安全。
- Flutter 卡顿场景里,先区分“网络慢”还是“解析/计算把 UI isolate 占住”。
- Android 场景里,先区分“主线程阻塞”还是“后台线程池/锁竞争把结果拖慢”。
5. 取消、超时、生命周期治理
| 语言 | 核心治理手段 | 最容易踩的坑 |
|---|---|---|
| Java | Future.cancel()、线程中断、线程池生命周期 | 只会启动,不会停;中断约定不一致 |
| Kotlin | CoroutineScope、Job、结构化并发、SupervisorJob | GlobalScope 乱飞,页面销毁后协程还跑 |
| Dart / Flutter | 手动协议、页面生命周期管理、isolate 生命周期 | 以为 Future 天然跟页面生命周期绑定 |
| JS | AbortController、组件卸载清理、Worker 终止 | 页面已离开但异步回调还回写状态 |
| Go | context、Done()、deadline、cancel | goroutine 泄漏、忘记 cancel() |
工程结论
- Kotlin 和 Go 在“取消传播”这件事上最成体系:一个靠
Job/作用域,一个靠context。 - Dart / JS 更需要工程师自己建立协议,不然最容易留下“页面没了,任务还在”的尾巴。
- Java 能做取消,但体系化程度比 Kotlin / Go 更依赖约定和工程纪律。
一张总表:先拿它做面试或设计前的快速校准
| 维度 | Java | Kotlin | Dart / Flutter | JS / Web | Go |
|---|---|---|---|---|---|
| 主执行模型 | 多线程 + 共享堆 | 多线程 + 协程挂起恢复 | 单 isolate event loop | 单线程 event loop | goroutine + runtime 调度 |
| 最小任务语义 | Runnable / Callable / Future | launch / async / suspend | Future / async | Promise / async | goroutine |
| 真正并发单元 | Thread | 仍是线程 | isolate | Worker | goroutine(映射到多线程) |
| 共享数据方式 | 共享内存 | 共享内存 | isolate 间消息传递 | Worker 间消息传递 | channel / 共享内存都可,但鼓励通信优先 |
| 恢复机制 | 线程继续执行 / 调度器唤醒 | Continuation + Dispatcher | event loop 恢复 continuation | microtask/macrotask 恢复回调 | runtime 调度继续运行 |
| 常见重活迁移方式 | 专门线程池 | Dispatchers.Default / 专门池 | Isolate.run / compute | Worker | 限流后的 goroutine / worker pool |
| 首要排障对象 | 锁、线程池、JMM、竞态 | Scope、Dispatcher、取消传播、共享状态 | 主 isolate 卡顿、队列顺序、isolate 成本 | 主线程长任务、队列优先级、Worker 边界 | goroutine 泄漏、channel 堵塞、context 传播 |
Android 背景 Flutter 工程师最值得带走的迁移判断
1. 从 Java/Kotlin 迁到 Dart 时,先扔掉“Future≈线程”直觉
在 JVM 世界里,你很容易把“异步”联想到线程池、后台线程、锁和共享状态;但到 Dart:
Future往往只是 当前 isolate 里的调度await是 恢复后半段,不是“切线程回来”- 只有 isolate 才是“真正把 CPU 重活移出去”的手段
2. 从 Kotlin 协程迁到 Flutter 时,要保留“结构化治理”习惯,但别照搬实现心智
Kotlin 协程让你习惯:
Scope绑定生命周期Job负责取消传播Dispatcher决定在哪恢复
到 Flutter 后,这些好习惯仍然重要,但落地手段不同:
- Flutter 没有协程式
Job/Scope体系替你兜底 - 很多取消和退出约束要你自己设计
- isolate 启动、消息传递、回收协议是另一套成本模型
3. JS 和 Dart 很像,但别忽略 Flutter 宿主约束
JS 与 Dart 都有 event loop / microtask 心智,所以:
- Promise vs
Future - microtask vs
Future.microtask - Worker vs isolate
这些对照很顺手。但 Flutter 比纯 Web 多了:
- UI isolate 和渲染/平台通道协作边界
- 页面生命周期、宿主线程、插件桥接
- 大对象序列化与平台切换成本
所以不能只用前端脑补 Flutter 卡顿问题。
4. Go 最值得借鉴的不是语法,而是“停得干净”的治理意识
很多移动端工程师看 Go,只看到:
- goroutine 很轻
- channel 很方便
真正值得迁移回移动端工程心智的是:
- 并发任务不是只看怎么发起,更要看怎么退出
- timeout / cancel / backpressure 必须前置设计
- 泄漏不是只发生在内存对象,也发生在“永远不退出的执行单元”
这对 Kotlin 协程取消治理、Flutter isolate 生命周期设计都很有借鉴价值。
典型决策题:工程上怎么快速选
题 1:Flutter 页面首帧卡顿,大 JSON 解析放哪?
- 不要先答:
Future/await - 先答:看是否 CPU 密集且明显占住 UI isolate
- 如果是:优先考虑 Dart isolate / Flutter compute:真正的并发隔离
题 2:Android 仓库层同时发多个请求并汇总,最该先想什么?
- Java:线程池边界、阻塞等待位置、共享状态安全
- Kotlin:
coroutineScope/async、取消传播、结果汇总失败策略 - 不要只停在“能并发发出去”
题 3:一个前端/H5 同学说“我已经用了 Promise,所以不会卡 UI”怎么办?
- 先判断重活是否还在主线程
- Promise 只改变调度顺序,不自动提供并行隔离
- 真重活还是要看 Worker
题 4:一个 Go 服务 goroutine 越跑越多,移动端工程师该怎样类比理解?
- 类比 Kotlin 里协程没绑生命周期、取消没传下去
- 类比 Flutter 里 isolate 起了没收、消息通道一直悬挂
- 本质都是“执行单元退出治理失败”
推荐复习顺序
- 先读 运行时承载模型入口
- 再读本页,把五种语言放回同一张判断表
- 回到三条主线:
- 最后用 JS Event Loop / Promise / Worker 和 Go goroutine / channel / context 做边界校准
对应实验
| 主题 | Lab | 说明 |
|---|---|---|
| Java 线程池 / 结果句柄 / 队列行为 | thread-pool-basics · queue-strategy-compare | 对照“任务提交”和“真实线程治理”不是一回事 |
| Java 共享状态与原子性 | volatile-vs-atomic-vs-lock | 对照共享内存世界里为什么仅异步不够 |
| Kotlin 挂起恢复 / Dispatcher | coroutine-execution-flow | 观察 Main → IO → Main 恢复链 |
| Kotlin 取消传播 / 共享状态保护 | coroutine-mutex-stateflow | 观察结构化并发与 Mutex 的不同职责 |
| Dart event loop / isolate | dart-event-loop-isolate | 对照“调度顺序”和“真正隔离并发” |
复习检查题
为什么说 Kotlin 协程和 Dart
Future都能“顺序写异步”,但它们的恢复地基不同?答:Kotlin 协程的挂起恢复建立在状态机 +
Continuation+ Dispatcher 之上,恢复时仍要借 JVM 线程执行;DartFuture/await的恢复建立在当前 isolate 的 event loop 之上,后半段是被重新排队恢复的,不等于切到另一条线程。为什么 Android 背景工程师迁到 Flutter 时,最容易误判 CPU 卡顿问题?
答:因为容易把 JVM 世界里的“异步≈后台线程”直觉带到 Dart,把
Future误当成独立执行单元。实际上很多Future仍在 UI isolate 内排队,CPU 重活如果不搬到 isolate,页面仍会卡顿。Go 的
context与 Kotlin 的Job最值得类比的共同点是什么?答:两者都在解决任务树里的取消/超时传播,不只是“把任务启动起来”,而是让任务能沿调用链正确停下来。
JS Promise、Dart
Future、JavaFuture这三个名字都叫 Future/Promise,为什么不能当成同一类东西来背?答:因为它们主要解决的问题不同:JS Promise 和 Dart
Future更强调事件循环里的异步结果编排与后半段恢复;JavaFuture更像线程池/任务执行结果的句柄,本身不提供 JS/Dart 那种统一的事件循环恢复语义。
速记
- Java:共享内存并发,重点是线程、锁、JMM、线程池
- Kotlin:线程之上的挂起恢复,重点是 Continuation、Dispatcher、Scope、Job
- Dart:event loop + isolate,重点是
Future不等于线程 - JS:Promise 只是调度,不是并行;重活靠 Worker
- Go:goroutine 很轻,但真正难点是 channel / context / 退出治理
- 先问模型,再选 API