【Kotlin + Spring Boot 4 从零到架构师】第 9 篇:协程基础(Kotlin 异步编程)
本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。
本篇你将学到
- 为什么需要协程------从回调地狱到结构化并发
suspend挂起函数的原理与使用launch与async/await两种启动模式- 协程作用域与结构化并发
- 调度器:Dispatchers.IO / Default / Main 的选择
- 协程异常处理与取消机制
学完本篇,你将理解协程的核心概念,能编写异步非阻塞的 Kotlin 代码,为后续 Spring Boot 中的协程应用打下基础。
一、为什么需要协程
1.1 异步编程的演进
考虑一个常见需求:从网络获取数据,处理后显示。这个过程是耗时的,不能阻塞主线程。
阶段一:回调(Callback)
kotlin
// 回调地狱------层层嵌套,难以阅读和维护
fetchUser { user ->
fetchOrders(user.id) { orders ->
fetchOrderDetails(orders.first().id) { details ->
fetchProductInfo(details.productId) { product ->
// 终于拿到数据了...
println(product)
}
}
}
}
阶段二:Java CompletableFuture
java
// Java CompletableFuture------链式调用,但仍然繁琐
fetchUser(userId)
.thenCompose(user -> fetchOrders(user.getId()))
.thenCompose(orders -> fetchOrderDetails(orders.get(0).getId()))
.thenCompose(details -> fetchProductInfo(details.getProductId()))
.thenAccept(product -> System.out.println(product));
阶段三:Kotlin 协程------以同步风格写异步代码
kotlin
// 协程------看起来像同步代码,实际是异步执行的
suspend fun loadProductInfo(): Product {
val user = fetchUser() // 异步,但不阻塞
val orders = fetchOrders(user.id) // 等上一步完成后执行
val details = fetchOrderDetails(orders.first().id)
val product = fetchProductInfo(details.productId)
return product
}
协程的核心价值:用同步的代码风格实现异步的非阻塞执行。代码从上往下读,逻辑一目了然,但底层是异步的。
1.2 什么是协程
协程是一种轻量级线程。它不是由操作系统调度的线程,而是由 Kotlin 运行时管理的「可挂起的计算单元」。
| 特性 | 线程 | 协程 |
|---|---|---|
| 创建成本 | 高(OS 级别,MB 级栈空间) | 极低(用户级别,KB 级) |
| 数量上限 | 几百到几千 | 十万级以上 |
| 切换成本 | 内核态切换,微秒级 | 用户态切换,纳秒级 |
| 阻塞 | 阻塞线程,占用资源 | 挂起不阻塞线程,释放线程 |
| 取消 | 不安全(强制 stop) | 安全(协作式取消) |
kotlin
// 启动 10 万个协程完全没问题
runBlocking {
repeat(100_000) {
launch {
delay(1000)
}
}
}
// 启动 10 万个线程?JVM 直接 OOM 崩溃
下面是异步编程演进的可视化对比:
#mermaid-svg-euNnqmuCTPZ6ooEU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-euNnqmuCTPZ6ooEU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-euNnqmuCTPZ6ooEU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-euNnqmuCTPZ6ooEU .error-icon{fill:#552222;}#mermaid-svg-euNnqmuCTPZ6ooEU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-euNnqmuCTPZ6ooEU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-euNnqmuCTPZ6ooEU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-euNnqmuCTPZ6ooEU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-euNnqmuCTPZ6ooEU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-euNnqmuCTPZ6ooEU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-euNnqmuCTPZ6ooEU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-euNnqmuCTPZ6ooEU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-euNnqmuCTPZ6ooEU .marker.cross{stroke:#333333;}#mermaid-svg-euNnqmuCTPZ6ooEU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-euNnqmuCTPZ6ooEU p{margin:0;}#mermaid-svg-euNnqmuCTPZ6ooEU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-euNnqmuCTPZ6ooEU .cluster-label text{fill:#333;}#mermaid-svg-euNnqmuCTPZ6ooEU .cluster-label span{color:#333;}#mermaid-svg-euNnqmuCTPZ6ooEU .cluster-label span p{background-color:transparent;}#mermaid-svg-euNnqmuCTPZ6ooEU .label text,#mermaid-svg-euNnqmuCTPZ6ooEU span{fill:#333;color:#333;}#mermaid-svg-euNnqmuCTPZ6ooEU .node rect,#mermaid-svg-euNnqmuCTPZ6ooEU .node circle,#mermaid-svg-euNnqmuCTPZ6ooEU .node ellipse,#mermaid-svg-euNnqmuCTPZ6ooEU .node polygon,#mermaid-svg-euNnqmuCTPZ6ooEU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-euNnqmuCTPZ6ooEU .rough-node .label text,#mermaid-svg-euNnqmuCTPZ6ooEU .node .label text,#mermaid-svg-euNnqmuCTPZ6ooEU .image-shape .label,#mermaid-svg-euNnqmuCTPZ6ooEU .icon-shape .label{text-anchor:middle;}#mermaid-svg-euNnqmuCTPZ6ooEU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-euNnqmuCTPZ6ooEU .rough-node .label,#mermaid-svg-euNnqmuCTPZ6ooEU .node .label,#mermaid-svg-euNnqmuCTPZ6ooEU .image-shape .label,#mermaid-svg-euNnqmuCTPZ6ooEU .icon-shape .label{text-align:center;}#mermaid-svg-euNnqmuCTPZ6ooEU .node.clickable{cursor:pointer;}#mermaid-svg-euNnqmuCTPZ6ooEU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-euNnqmuCTPZ6ooEU .arrowheadPath{fill:#333333;}#mermaid-svg-euNnqmuCTPZ6ooEU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-euNnqmuCTPZ6ooEU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-euNnqmuCTPZ6ooEU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-euNnqmuCTPZ6ooEU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-euNnqmuCTPZ6ooEU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-euNnqmuCTPZ6ooEU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-euNnqmuCTPZ6ooEU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-euNnqmuCTPZ6ooEU .cluster text{fill:#333;}#mermaid-svg-euNnqmuCTPZ6ooEU .cluster span{color:#333;}#mermaid-svg-euNnqmuCTPZ6ooEU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-euNnqmuCTPZ6ooEU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-euNnqmuCTPZ6ooEU rect.text{fill:none;stroke-width:0;}#mermaid-svg-euNnqmuCTPZ6ooEU .icon-shape,#mermaid-svg-euNnqmuCTPZ6ooEU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-euNnqmuCTPZ6ooEU .icon-shape p,#mermaid-svg-euNnqmuCTPZ6ooEU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-euNnqmuCTPZ6ooEU .icon-shape .label rect,#mermaid-svg-euNnqmuCTPZ6ooEU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-euNnqmuCTPZ6ooEU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-euNnqmuCTPZ6ooEU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-euNnqmuCTPZ6ooEU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 演进
演进
阶段三:Kotlin 协程
val user = fetchUser()
val orders = fetchOrders(user.id)
val details = fetchOrderDetails(...)
val product = fetchProductInfo(...)
return product
阶段二:CompletableFuture
fetchUser
thenCompose
thenCompose
thenCompose
thenAccept
阶段一:回调地狱
fetchUser
fetchOrders
fetchOrderDetails
fetchProductInfo
println(product)
二、第一个协程
2.1 添加协程依赖
在 build.gradle.kts 中添加:
kotlin
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}
如果是 Spring Boot 项目,
spring-boot-starter-web已经间接包含了协程依赖,通常不需要额外添加。
2.2 启动协程
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// launch 启动一个新协程
launch {
delay(1000) // 挂起 1 秒(不阻塞线程)
println("World!") // 1 秒后输出
}
println("Hello") // 立即输出
}
// 输出顺序:
// Hello (立即)
// World! (1 秒后)
runBlocking:阻塞当前线程,直到协程完成。仅用于顶层测试和 main 函数,不要在生产代码中使用。launch:启动一个新协程,不返回结果(返回Job)。delay:挂起当前协程指定时间,不阻塞底层线程。
2.3 suspend 关键字
suspend 标记一个函数为挂起函数。挂起函数只能在协程内部或其他挂起函数中调用:
kotlin
// 挂起函数:可以暂停和恢复
suspend fun fetchData(): String {
delay(1000) // 模拟耗时操作(网络请求等)
return "数据加载完成"
}
// 在协程中调用
fun main() = runBlocking {
println("开始")
val data = fetchData() // 调用挂起函数,等它完成
println(data)
}
// 输出:
// 开始
// (等待 1 秒)
// 数据加载完成
关键理解 :
suspend函数挂起时,不会阻塞当前线程------线程可以去执行其他协程。当挂起操作完成(如网络响应到达),协程恢复执行。这就是协程高效的秘密。
三、launch 与 async
3.1 launch------启动不返回结果的协程
kotlin
fun main() = runBlocking {
// launch 返回一个 Job 对象
val job1 = launch {
delay(1000)
println("任务 1 完成")
}
val job2 = launch {
delay(500)
println("任务 2 完成")
}
// 等待所有协程完成
job1.join() // 等待 job1 完成
job2.join() // 等待 job2 完成
println("所有任务完成")
}
// 输出:
// 任务 2 完成(0.5 秒)
// 任务 1 完成(1 秒)
// 所有任务完成
3.2 async------启动并返回结果
kotlin
fun main() = runBlocking {
// async 返回一个 Deferred<T>,可以用 await() 获取结果
val deferred1 = async {
delay(1000)
"第一个结果"
}
val deferred2 = async {
delay(800)
42
}
// await() 会挂起直到结果就绪
val result1 = deferred1.await()
val result2 = deferred2.await()
println("$result1, $result2")
}
// 输出:第一个结果, 42
3.3 并发执行
async 最强大的场景是并发执行多个独立任务:
kotlin
suspend fun fetchUserInfo(): String {
delay(1000) // 模拟网络请求
return "用户信息"
}
suspend fun fetchOrders(): List<String> {
delay(1000) // 模拟网络请求
return listOf("订单1", "订单2")
}
fun main() = runBlocking {
// 串行执行:总耗时 = 1s + 1s = 2s
val serialStart = System.currentTimeMillis()
val user1 = fetchUserInfo() // 1 秒
val orders1 = fetchOrders() // 再 1 秒
println("串行耗时:${System.currentTimeMillis() - serialStart}ms")
// 并发执行:总耗时 = max(1s, 1s) = 1s
val concurrentStart = System.currentTimeMillis()
val userDeferred = async { fetchUserInfo() }
val ordersDeferred = async { fetchOrders() }
val user2 = userDeferred.await()
val orders2 = ordersDeferred.await()
println("并发耗时:${System.currentTimeMillis() - concurrentStart}ms")
}
// 输出:
// 串行耗时:~2000ms
// 并发耗时:~1000ms
3.4 awaitAll------等待所有结果
kotlin
fun main() = runBlocking {
val results = listOf(
async { fetchUserInfo() },
async { fetchOrders() },
async { delay(500); "推荐商品" }
).awaitAll() // 等待全部完成
results.forEach { println(it) }
}
3.5 launch vs async 选择指南
| 特性 | launch | async |
|---|---|---|
| 返回值 | Job(无结果) |
Deferred<T>(有结果) |
| 异常处理 | 直接传播到父协程 | 仅在 await() 时抛出 |
| 适用场景 | 副作用操作(日志、保存、通知) | 需要获取返回值 |
原则 :需要结果用
async,不需要结果用launch。不要用async仅仅为了「开一个后台线程」却不await结果。
下面是 launch 与 async 的选择决策流程:
#mermaid-svg-LkfID1QDxFNDE4mA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LkfID1QDxFNDE4mA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LkfID1QDxFNDE4mA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LkfID1QDxFNDE4mA .error-icon{fill:#552222;}#mermaid-svg-LkfID1QDxFNDE4mA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LkfID1QDxFNDE4mA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LkfID1QDxFNDE4mA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LkfID1QDxFNDE4mA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LkfID1QDxFNDE4mA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LkfID1QDxFNDE4mA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LkfID1QDxFNDE4mA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LkfID1QDxFNDE4mA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LkfID1QDxFNDE4mA .marker.cross{stroke:#333333;}#mermaid-svg-LkfID1QDxFNDE4mA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LkfID1QDxFNDE4mA p{margin:0;}#mermaid-svg-LkfID1QDxFNDE4mA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LkfID1QDxFNDE4mA .cluster-label text{fill:#333;}#mermaid-svg-LkfID1QDxFNDE4mA .cluster-label span{color:#333;}#mermaid-svg-LkfID1QDxFNDE4mA .cluster-label span p{background-color:transparent;}#mermaid-svg-LkfID1QDxFNDE4mA .label text,#mermaid-svg-LkfID1QDxFNDE4mA span{fill:#333;color:#333;}#mermaid-svg-LkfID1QDxFNDE4mA .node rect,#mermaid-svg-LkfID1QDxFNDE4mA .node circle,#mermaid-svg-LkfID1QDxFNDE4mA .node ellipse,#mermaid-svg-LkfID1QDxFNDE4mA .node polygon,#mermaid-svg-LkfID1QDxFNDE4mA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LkfID1QDxFNDE4mA .rough-node .label text,#mermaid-svg-LkfID1QDxFNDE4mA .node .label text,#mermaid-svg-LkfID1QDxFNDE4mA .image-shape .label,#mermaid-svg-LkfID1QDxFNDE4mA .icon-shape .label{text-anchor:middle;}#mermaid-svg-LkfID1QDxFNDE4mA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LkfID1QDxFNDE4mA .rough-node .label,#mermaid-svg-LkfID1QDxFNDE4mA .node .label,#mermaid-svg-LkfID1QDxFNDE4mA .image-shape .label,#mermaid-svg-LkfID1QDxFNDE4mA .icon-shape .label{text-align:center;}#mermaid-svg-LkfID1QDxFNDE4mA .node.clickable{cursor:pointer;}#mermaid-svg-LkfID1QDxFNDE4mA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LkfID1QDxFNDE4mA .arrowheadPath{fill:#333333;}#mermaid-svg-LkfID1QDxFNDE4mA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LkfID1QDxFNDE4mA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LkfID1QDxFNDE4mA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LkfID1QDxFNDE4mA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LkfID1QDxFNDE4mA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LkfID1QDxFNDE4mA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LkfID1QDxFNDE4mA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LkfID1QDxFNDE4mA .cluster text{fill:#333;}#mermaid-svg-LkfID1QDxFNDE4mA .cluster span{color:#333;}#mermaid-svg-LkfID1QDxFNDE4mA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LkfID1QDxFNDE4mA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LkfID1QDxFNDE4mA rect.text{fill:none;stroke-width:0;}#mermaid-svg-LkfID1QDxFNDE4mA .icon-shape,#mermaid-svg-LkfID1QDxFNDE4mA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LkfID1QDxFNDE4mA .icon-shape p,#mermaid-svg-LkfID1QDxFNDE4mA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LkfID1QDxFNDE4mA .icon-shape .label rect,#mermaid-svg-LkfID1QDxFNDE4mA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LkfID1QDxFNDE4mA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LkfID1QDxFNDE4mA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LkfID1QDxFNDE4mA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
需要启动一个协程
是否需要
获取返回值?
使用 async
使用 launch
返回 Deferred<T>
通过 await() 获取结果
返回 Job
用于控制协程生命周期
是否只是
为了开后台线程?
不要这样做!
应该用 launch
正确用法
是否需要
等待完成?
使用 job.join()
启动即忘
四、结构化并发
4.1 协程作用域
每个协程都运行在一个协程作用域(CoroutineScope)中。作用域定义了协程的生命周期------当作用域取消时,里面所有协程都会被取消。
kotlin
fun main() = runBlocking {
// 外层协程是内层协程的「父」
launch {
delay(1000)
println("外层协程")
launch {
delay(500)
println("内层协程") // 外层完成后才执行内层
}
}
println("主协程")
}
4.2 父子协程的取消传播
当父协程被取消时,所有子协程也会自动取消:
kotlin
fun main() = runBlocking {
val parentJob = launch {
// 子协程 1
launch {
repeat(10) { i ->
delay(300)
println("子协程 1 - 第 $i 次")
}
}
// 子协程 2
launch {
repeat(10) { i ->
delay(300)
println("子协程 2 - 第 $i 次")
}
}
}
delay(1000) // 让子协程运行一会儿
println("取消父协程")
parentJob.cancel() // 取消父协程,子协程全部自动取消
parentJob.join()
println("全部停止")
}
// 子协程 1 和 2 各运行几次后被一起取消
结构化并发的核心保证:协程不会「泄漏」。当外部作用域结束时,内部所有协程都会被清理,不会出现孤立的协程。
4.3 coroutineScope 创建作用域
kotlin
suspend fun loadAllData() = coroutineScope {
// 在这个作用域内启动的协程全部完成后,函数才返回
val user = async { fetchUserInfo() }
val orders = async { fetchOrders() }
println("${user.await()}, ${orders.await()}")
}
fun main() = runBlocking {
loadAllData()
// loadAllData 返回意味着它内部所有协程都已完成
}
coroutineScope是最常用的作用域构建器------它等待所有子协程完成后才返回自身,并且任何一个子协程的异常都会导致整个作用域失败。
4.4 supervisorScope------子协程互不影响
kotlin
suspend fun runIndependentTasks() = supervisorScope {
// supervisorScope:一个子协程失败不会影响其他子协程
launch {
delay(500)
throw RuntimeException("子协程 1 崩溃了")
}
launch {
delay(1000)
println("子协程 2 仍然正常完成") // 不受子协程 1 影响
}
}
| 作用域 | 子协程异常影响 | 适用场景 |
|---|---|---|
coroutineScope |
一个失败,全部取消 | 有关联的并发任务 |
supervisorScope |
互不影响 | 独立的并发任务 |
下面是结构化并发中父子协程取消传播的时序图:
"子协程 2" "子协程 1" "父协程" "主协程" "子协程 2" "子协程 1" "父协程" "主协程" #mermaid-svg-5vZdcFRRYXj3hDbw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5vZdcFRRYXj3hDbw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5vZdcFRRYXj3hDbw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5vZdcFRRYXj3hDbw .error-icon{fill:#552222;}#mermaid-svg-5vZdcFRRYXj3hDbw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5vZdcFRRYXj3hDbw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5vZdcFRRYXj3hDbw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5vZdcFRRYXj3hDbw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5vZdcFRRYXj3hDbw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5vZdcFRRYXj3hDbw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5vZdcFRRYXj3hDbw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5vZdcFRRYXj3hDbw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5vZdcFRRYXj3hDbw .marker.cross{stroke:#333333;}#mermaid-svg-5vZdcFRRYXj3hDbw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5vZdcFRRYXj3hDbw p{margin:0;}#mermaid-svg-5vZdcFRRYXj3hDbw .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5vZdcFRRYXj3hDbw text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-5vZdcFRRYXj3hDbw .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5vZdcFRRYXj3hDbw .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-5vZdcFRRYXj3hDbw .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-5vZdcFRRYXj3hDbw .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-5vZdcFRRYXj3hDbw #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-5vZdcFRRYXj3hDbw .sequenceNumber{fill:white;}#mermaid-svg-5vZdcFRRYXj3hDbw #sequencenumber{fill:#333;}#mermaid-svg-5vZdcFRRYXj3hDbw #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-5vZdcFRRYXj3hDbw .messageText{fill:#333;stroke:none;}#mermaid-svg-5vZdcFRRYXj3hDbw .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5vZdcFRRYXj3hDbw .labelText,#mermaid-svg-5vZdcFRRYXj3hDbw .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-5vZdcFRRYXj3hDbw .loopText,#mermaid-svg-5vZdcFRRYXj3hDbw .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-5vZdcFRRYXj3hDbw .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5vZdcFRRYXj3hDbw .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-5vZdcFRRYXj3hDbw .noteText,#mermaid-svg-5vZdcFRRYXj3hDbw .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-5vZdcFRRYXj3hDbw .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5vZdcFRRYXj3hDbw .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5vZdcFRRYXj3hDbw .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5vZdcFRRYXj3hDbw .actorPopupMenu{position:absolute;}#mermaid-svg-5vZdcFRRYXj3hDbw .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-5vZdcFRRYXj3hDbw .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5vZdcFRRYXj3hDbw .actor-man circle,#mermaid-svg-5vZdcFRRYXj3hDbw line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-5vZdcFRRYXj3hDbw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} launch { ... } launch { repeat(10) } launch { repeat(10) } 运行中... 运行中... parentJob.cancel() 自动取消 自动取消 已停止 已停止 全部停止
五、调度器
调度器决定协程在哪个线程上运行。
5.1 三种内置调度器
| 调度器 | 适用场景 | 线程池 |
|---|---|---|
Dispatchers.Main |
UI 操作(Android、JavaFX) | 单线程(主线程) |
Dispatchers.IO |
网络、文件、数据库等 IO 操作 | 按需创建(默认 64 个) |
Dispatchers.Default |
CPU 密集型计算(排序、解析、过滤) | CPU 核心数 |
5.2 切换调度器
kotlin
fun main() = runBlocking {
launch(Dispatchers.Default) {
println("CPU 计算在 ${Thread.currentThread().name}")
// 在 Default 线程池执行
}
launch(Dispatchers.IO) {
println("IO 操作在 ${Thread.currentThread().name}")
// 在 IO 线程池执行
}
// withContext:切换到指定调度器执行,完成后切回来
val result = withContext(Dispatchers.IO) {
delay(500)
"从 IO 线程返回的数据"
}
println(result)
}
5.3 withContext------切换线程的利器
kotlin
suspend fun saveToDatabase(data: String) {
// 切换到 IO 线程执行数据库操作
withContext(Dispatchers.IO) {
// 执行数据库写入
delay(500)
println("已保存到数据库:$data")
}
// 自动切回原来的调度器
}
suspend fun processData(data: String): String {
// 切换到 Default 线程做 CPU 密集计算
return withContext(Dispatchers.Default) {
data.uppercase() // 模拟计算
}
}
在 Spring Boot 中 :Spring 的协程支持会自动配置调度器。Controller 中的
suspend函数默认运行在专门的调度器上,withContext用于切换到特定线程池。
下面是协程调度器切换的流程图:
#mermaid-svg-irrobxdp8yZ4B0Ty{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-irrobxdp8yZ4B0Ty .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-irrobxdp8yZ4B0Ty .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-irrobxdp8yZ4B0Ty .error-icon{fill:#552222;}#mermaid-svg-irrobxdp8yZ4B0Ty .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-irrobxdp8yZ4B0Ty .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-irrobxdp8yZ4B0Ty .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-irrobxdp8yZ4B0Ty .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-irrobxdp8yZ4B0Ty .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-irrobxdp8yZ4B0Ty .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-irrobxdp8yZ4B0Ty .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-irrobxdp8yZ4B0Ty .marker{fill:#333333;stroke:#333333;}#mermaid-svg-irrobxdp8yZ4B0Ty .marker.cross{stroke:#333333;}#mermaid-svg-irrobxdp8yZ4B0Ty svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-irrobxdp8yZ4B0Ty p{margin:0;}#mermaid-svg-irrobxdp8yZ4B0Ty .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-irrobxdp8yZ4B0Ty .cluster-label text{fill:#333;}#mermaid-svg-irrobxdp8yZ4B0Ty .cluster-label span{color:#333;}#mermaid-svg-irrobxdp8yZ4B0Ty .cluster-label span p{background-color:transparent;}#mermaid-svg-irrobxdp8yZ4B0Ty .label text,#mermaid-svg-irrobxdp8yZ4B0Ty span{fill:#333;color:#333;}#mermaid-svg-irrobxdp8yZ4B0Ty .node rect,#mermaid-svg-irrobxdp8yZ4B0Ty .node circle,#mermaid-svg-irrobxdp8yZ4B0Ty .node ellipse,#mermaid-svg-irrobxdp8yZ4B0Ty .node polygon,#mermaid-svg-irrobxdp8yZ4B0Ty .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-irrobxdp8yZ4B0Ty .rough-node .label text,#mermaid-svg-irrobxdp8yZ4B0Ty .node .label text,#mermaid-svg-irrobxdp8yZ4B0Ty .image-shape .label,#mermaid-svg-irrobxdp8yZ4B0Ty .icon-shape .label{text-anchor:middle;}#mermaid-svg-irrobxdp8yZ4B0Ty .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-irrobxdp8yZ4B0Ty .rough-node .label,#mermaid-svg-irrobxdp8yZ4B0Ty .node .label,#mermaid-svg-irrobxdp8yZ4B0Ty .image-shape .label,#mermaid-svg-irrobxdp8yZ4B0Ty .icon-shape .label{text-align:center;}#mermaid-svg-irrobxdp8yZ4B0Ty .node.clickable{cursor:pointer;}#mermaid-svg-irrobxdp8yZ4B0Ty .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-irrobxdp8yZ4B0Ty .arrowheadPath{fill:#333333;}#mermaid-svg-irrobxdp8yZ4B0Ty .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-irrobxdp8yZ4B0Ty .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-irrobxdp8yZ4B0Ty .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-irrobxdp8yZ4B0Ty .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-irrobxdp8yZ4B0Ty .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-irrobxdp8yZ4B0Ty .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-irrobxdp8yZ4B0Ty .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-irrobxdp8yZ4B0Ty .cluster text{fill:#333;}#mermaid-svg-irrobxdp8yZ4B0Ty .cluster span{color:#333;}#mermaid-svg-irrobxdp8yZ4B0Ty div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-irrobxdp8yZ4B0Ty .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-irrobxdp8yZ4B0Ty rect.text{fill:none;stroke-width:0;}#mermaid-svg-irrobxdp8yZ4B0Ty .icon-shape,#mermaid-svg-irrobxdp8yZ4B0Ty .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-irrobxdp8yZ4B0Ty .icon-shape p,#mermaid-svg-irrobxdp8yZ4B0Ty .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-irrobxdp8yZ4B0Ty .icon-shape .label rect,#mermaid-svg-irrobxdp8yZ4B0Ty .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-irrobxdp8yZ4B0Ty .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-irrobxdp8yZ4B0Ty .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-irrobxdp8yZ4B0Ty :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 未指定
Dispatchers.Main
Dispatchers.Default
Dispatchers.IO
是
否
协程启动
指定调度器?
继承父协程的调度器
主线程
(UI 操作)
CPU 密集型
(排序、解析)
IO 密集型
(网络、文件、数据库)
需要切换
线程池?
withContext(Dispatchers.XXX)
执行任务
自动切回原调度器
继续执行
六、协程取消与超时
6.1 取消协程
kotlin
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
// isActive 检查是否被取消
if (!isActive) return@launch // 主动退出
println("运行中:$i")
delay(500)
}
}
delay(2000)
println("发起取消")
job.cancel() // 取消协程
job.join() // 等待取消完成
println("已取消")
}
协作式取消 :Kotlin 协程的取消是协作式的------调用
cancel()只是设置了一个标志,协程需要自己检查并退出。delay、await等挂起函数会自动检查取消状态。
6.2 超时控制
kotlin
fun main() = runBlocking {
try {
// withTimeout:超时自动取消
val result = withTimeoutOrNull(2000) {
repeat(10) { i ->
println("处理 $i")
delay(500)
}
"全部完成"
}
println("结果:$result") // 如果超时返回 null
} finally {
println("清理资源")
}
}
| 函数 | 超时行为 |
|---|---|
withTimeout(ms) |
超时抛出 TimeoutCancellationException |
withTimeoutOrNull(ms) |
超时返回 null(推荐,不抛异常) |
七、实战练习
练习:并发数据加载器
kotlin
import kotlinx.coroutines.*
// 模拟网络请求
suspend fun fetchUserProfile(userId: String): String {
delay(800)
return "用户($userId)的资料"
}
suspend fun fetchOrderHistory(userId: String): List<String> {
delay(1200)
return listOf("订单A", "订单B", "订单C")
}
suspend fun fetchRecommendations(userId: String): List<String> {
delay(600)
return listOf("推荐商品1", "推荐商品2")
}
fun main() = runBlocking {
val userId = "U001"
// 串行方式(慢)
val serialStart = System.currentTimeMillis()
val profile1 = fetchUserProfile(userId)
val orders1 = fetchOrderHistory(userId)
val recs1 = fetchRecommendations(userId)
val serialTime = System.currentTimeMillis() - serialStart
println("串行总耗时:${serialTime}ms")
println("---")
// 并发方式(快)
val concurrentStart = System.currentTimeMillis()
val profile2 = async { fetchUserProfile(userId) }
val orders2 = async { fetchOrderHistory(userId) }
val recs2 = async { fetchRecommendations(userId) }
// 可以先处理先完成的结果
val profile = profile2.await()
println("用户资料:$profile")
val recs = recs2.await()
println("推荐:$recs")
val orders = orders2.await()
println("订单:$orders")
val concurrentTime = System.currentTimeMillis() - concurrentStart
println("并发总耗时:${concurrentTime}ms")
// 串行约 2600ms,并发约 1200ms(取决于最慢的任务)
}
本篇小结
| 知识点 | 核心内容 |
|---|---|
| 协程 | 轻量级线程,用户态调度,以同步风格写异步代码 |
suspend |
标记挂起函数,只能在协程中调用 |
delay |
挂起(不阻塞线程),等待指定时间 |
launch |
启动协程,返回 Job(无返回值) |
async |
启动协程,返回 Deferred<T>(有返回值) |
await() |
等待 Deferred 的结果 |
awaitAll() |
等待多个 Deferred 全部完成 |
coroutineScope |
创建作用域,等待所有子协程完成 |
supervisorScope |
子协程互不影响 |
Dispatchers.IO |
网络/文件/数据库 IO |
Dispatchers.Default |
CPU 密集计算 |
withContext |
切换调度器,执行完后自动切回 |
withTimeoutOrNull |
超时控制,返回 null |
| 结构化并发 | 父协程取消时子协程自动取消 |
模块一总结
恭喜你完成了 Kotlin 语言基础模块!让我们回顾一下这 9 篇的完整知识体系:
| 篇 | 主题 | 核心收获 |
|---|---|---|
| 1 | 环境搭建 | JDK 21 + IDEA,第一个程序 |
| 2 | 变量与空安全 | val/var,空安全操作符 |
| 3 | 控制流 | if 表达式,when,区间,循环 |
| 4 | 函数与 Lambda | 默认参数,高阶函数,集合操作 |
| 5 | 类与对象 | data class,属性,枚举 |
| 6 | 继承与接口 | open/override,密封类,智能转换 |
| 7 | 扩展与作用域函数 | 扩展函数,apply/let/also |
| 8 | 泛型与集合 | 只读/可变集合,out/in,reified |
| 9 | 协程 | launch/async,结构化并发,调度器 |
你已经掌握了 Kotlin 语言的核心语法。从下一篇开始,我们将正式进入 Spring Boot 4 的世界------创建项目、依赖注入、REST API,一步步构建后端应用。
下篇预告
Spring Boot 到底解决了什么问题?IoC 和 DI 是什么意思?下一篇我们走进 Spring 生态,创建参考项目
mini-shop的第一个 API。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。