第09篇-协程基础-Kotlin异步编程

【Kotlin + Spring Boot 4 从零到架构师】第 9 篇:协程基础(Kotlin 异步编程)

本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。


本篇你将学到

  • 为什么需要协程------从回调地狱到结构化并发
  • suspend 挂起函数的原理与使用
  • launchasync/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 结果。


下面是 launchasync 的选择决策流程:
#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() 只是设置了一个标志,协程需要自己检查并退出。delayawait 等挂起函数会自动检查取消状态。

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/inreified
9 协程 launch/async,结构化并发,调度器

你已经掌握了 Kotlin 语言的核心语法。从下一篇开始,我们将正式进入 Spring Boot 4 的世界------创建项目、依赖注入、REST API,一步步构建后端应用。


下篇预告

第 10 篇:Spring 生态全景与项目初始化

Spring Boot 到底解决了什么问题?IoC 和 DI 是什么意思?下一篇我们走进 Spring 生态,创建参考项目 mini-shop 的第一个 API。


如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。

相关推荐
其实防守也摸鱼1 小时前
HackBar 工具完全指南:信息探测、漏洞验证与安全测试实战
开发语言·人工智能·学习·安全·网络安全·安全威胁分析·安全性测试
会博通·代码搬运工1 小时前
会博通API对接实战:工程企业文档分布式采集系统的技术实现与Python SDK详解
开发语言·分布式·python·线性代数·矩阵·架构·电子档案合规
林森lsjs1 小时前
完结撒花!Java SE 语法阶段总结!
java·开发语言
lupai1 小时前
短信接口快速接入与调用实战指南
java·开发语言·数据库
刘名喜2 小时前
第12篇-Gradle-Kotlin-DSL构建指南
开发语言·kotlin·springboot
大鱼>2 小时前
DSPy:LLM程序自动编译与提示词优化
开发语言·人工智能·python·深度学习
撩妹帝九歌2 小时前
Java文件写入与编码、字节数组、字符集、字符编解码 一文打通!
java·开发语言
星核0penstarry2 小时前
Solon AI v4.0.4 技术分析:Java Agent 框架的兼容性突破与选型考量 基于 OSCHINA 2026-07-30 报道及公开技术资料整理
java·开发语言·人工智能
昨夜星河入梦来2 小时前
postman接口测试报错503的解决方法
开发语言·postman