Kotlin 协程与 suspend 原理详解

从"怎么写"到"编译器把它变成什么",再到"运行时怎么调度"的完整知识文档。


目录

  1. 第一部分:入门
    • [1.1 什么是 suspend 函数](#1.1 什么是 suspend 函数)
    • [1.2 最简单的 suspend 示例](#1.2 最简单的 suspend 示例)
    • [1.3 suspend 的调用限制](#1.3 suspend 的调用限制)
  2. [第二部分:一个 suspend 里调用多个 suspend](#第二部分:一个 suspend 里调用多个 suspend)
    • [2.1 串行调用](#2.1 串行调用)
    • [2.2 并发调用](#2.2 并发调用)
    • [2.3 串行 vs 并发对比](#2.3 串行 vs 并发对比)
  3. [第三部分:编译原理------suspend 被编译成什么](#第三部分:编译原理——suspend 被编译成什么)
    • [3.1 CPS 转换](#3.1 CPS 转换)
    • [3.2 状态机](#3.2 状态机)
    • [3.3 局部变量提升为字段](#3.3 局部变量提升为字段)
    • [3.4 多个挂起点:N+1 个状态](#3.4 多个挂起点:N+1 个状态)
    • [3.5 关键澄清:状态机是同步的,异步性来自挂起/恢复](#3.5 关键澄清:状态机是同步的,异步性来自挂起/恢复)
  4. 第四部分:运行时------谁调用、怎么进入线程
    • [4.1 runBlocking 做了什么](#4.1 runBlocking 做了什么)
    • [4.2 完整时序:loadAll 被进入三次](#4.2 完整时序:loadAll 被进入三次)
    • [4.3 结果如何向上传递](#4.3 结果如何向上传递)
  5. [第五部分:Dispatchers.IO 场景------多线程跳转](#第五部分:Dispatchers.IO 场景——多线程跳转)
  6. 第六部分:挂起点前后有普通代码会怎样
  7. 第七部分:总结与速查表

第一部分:入门

1.1 什么是 suspend 函数

suspend 是 Kotlin 用来标记可以被挂起的函数的修饰符。它的特点:

  • 可以在不阻塞线程的情况下暂停执行
  • 只能在协程 或其他 suspend 函数中调用
  • 挂起后,线程会被释放去干别的事,之后在某个时机"恢复"

1.2 最简单的 suspend 示例

kotlin 复制代码
import kotlinx.coroutines.*

suspend fun fetchData(): String {
    delay(1000)  // 挂起 1 秒,不阻塞线程
    return "数据加载完成"
}

fun main() = runBlocking {
    println("开始加载...")
    val result = fetchData()
    println(result)
}

输出:

复制代码
开始加载...
(等待 1 秒)
数据加载完成

关键点:

概念 说明
suspend 标记可挂起函数
delay() 挂起函数,不阻塞线程 (区别 Thread.sleep)
runBlocking 桥接阻塞世界与协程世界,常用于 main
依赖 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")

1.3 suspend 的调用限制

kotlin 复制代码
suspend fun a(): String = "a"

fun normalFun() {
    // ❌ 编译错误:Suspend function 'a' should be called only from a coroutine
    // a()
}

规则:suspend 函数只能被 suspend 函数或协程调用。


第二部分:一个 suspend 里调用多个 suspend

2.1 串行调用

kotlin 复制代码
suspend fun fetchUser(): String {
    delay(1000)
    return "用户信息"
}

suspend fun fetchOrder(): String {
    delay(1000)
    return "订单信息"
}

// 串行:一个执行完再执行下一个
suspend fun loadAll(): String {
    val user = fetchUser()    // 约 1 秒
    val order = fetchOrder()  // 约 1 秒
    return "$user + $order"
}

总耗时约 2000ms。

2.2 并发调用

kotlin 复制代码
suspend fun loadAllConcurrent(): String = coroutineScope {
    val userDeferred = async { fetchUser() }
    val orderDeferred = async { fetchOrder() }

    val user = userDeferred.await()
    val order = orderDeferred.await()

    "$user + $order"
}

总耗时约 1000ms。

简化写法:

kotlin 复制代码
suspend fun loadAllSimple(): String = coroutineScope {
    val results = listOf(
        async { fetchUser() },
        async { fetchOrder() }
    ).awaitAll()
    results.joinToString(" + ")
}

2.3 串行 vs 并发对比

方式 写法 耗时 适用场景
串行 依次调用 两者之和 后一个依赖前一个
并发 async { } + await() 较慢的那个 两个任务相互独立

注意 :async{}.await() 再 async{}.await() 会退化成串行。必须先启动所有 async,再统一 await。

异常处理:

kotlin 复制代码
suspend fun loadAllSafe(): String = coroutineScope {
    val user = async { runCatching { fetchUser() }.getOrDefault("失败") }
    val order = async { runCatching { fetchOrder() }.getOrDefault("失败") }
    "${user.await()} + ${order.await()}"
}

⚠️ async 失败会取消整个 coroutineScope。想独立处理异常,用 supervisorScope 或 runCatching。


第三部分:编译原理------suspend 被编译成什么

3.1 CPS 转换

suspend 函数会被编译成 CPS(Continuation Passing Style) 形式:多一个 Continuation 参数,返回值变成 Object。

kotlin 复制代码
suspend fun loadAll(): String

编译后签名大致是:

java 复制代码
Object loadAll(Continuation<? super String> continuation)

Continuation 接口:

java 复制代码
public interface Continuation<T> {
    void resumeWith(Object result);   // result 是 T 或 Throwable
    CoroutineContext getContext();
}

Object 返回值有两种可能:

  • 正常结果(最终 String)
  • COROUTINE_SUSPENDED 特殊标记(表示"我真的挂起了")

3.2 状态机

编译器把 loadAll 变成一个 switch + label 状态机。以 2.1 的串行版本为例:

kotlin 复制代码
suspend fun loadAll(): String {
    val user = fetchUser()    // 调用点 1
    val order = fetchOrder()  // 调用点 2
    return "$user + $order"
}

编译后等价 Java:

java 复制代码
public static Object loadAll(Continuation<? super String> completion) {
    LoadAllContinuation c = new LoadAllContinuation(completion);

    Object result;
    switch (c.label) {
        case 0:
            c.label = 1;                              // 下次从 case 1 恢复
            result = fetchUser(c);                    // ★ 调用点 1
            if (result == COROUTINE_SUSPENDED) {
                return COROUTINE_SUSPENDED;           // ★ 挂起,栈弹出
            }
            break;                                    // 没挂起,继续

        case 1:
            result = c.result;                        // 取回 user
            break;

        default:
            throw new IllegalStateException("call to 'resume' before 'invoke'");
    }

    String user = (String) result;
    c.user = user;                                    // ★ 提升为字段

    switch (c.label) {
        case 1:
            c.label = 2;                              // 下次从 case 2 恢复
            result = fetchOrder(c);                   // ★ 调用点 2
            if (result == COROUTINE_SUSPENDED) {
                return COROUTINE_SUSPENDED;
            }
            break;

        case 2:
            result = c.result;                        // 取回 order
            break;
    }

    String order = (String) result;
    c.order = order;

    return c.user + " + " + c.order;                  // ★ 真正返回
}

状态机 Continuation:

java 复制代码
final class LoadAllContinuation implements Continuation<Object> {
    int label = 0;
    Object result;
    String user;                          // ★ 局部变量提升
    String order;                         // ★ 局部变量提升
    final Continuation<? super String> completion;

    LoadAllContinuation(Continuation<? super String> completion) {
        this.completion = completion;
    }

    @Override
    public void resumeWith(Object result) {
        this.result = result;
        ExampleKt.loadAll(this);          // ★ 重新进入
    }

    @Override
    public CoroutineContext getContext() {
        return completion.getContext();
    }
}

三次进入 loadAll,label 不同:

进入时机 label 走哪个 case 行为
首次调用 0 case 0 调用 fetchUser
fetchUser 完成后 1 case 1 取回 user,调用 fetchOrder
fetchOrder 完成后 2 case 2 取回 order,返回结果

3.3 局部变量提升为字段

为什么? 一旦挂起(return),栈帧销毁,局部变量消失;恢复后还要用,所以必须提升为 Continuation 的字段。

java 复制代码
c.user = user;    // 挂起前保存
// ... 恢复后 ...
return c.user + " + " + c.order;   // 从字段取回

判断标准 :变量在某个挂起点之后是否还会被用到?会 → 提升;只在单段内用 → 保留为局部变量。

3.4 多个挂起点:N+1 个状态

N 个挂起点 → 有 N+1 个状态(label = 0...N)→ 代码被切成 N+1 段。

复制代码
label 0 ──[普通代码]──[挂起点1]──► label 1
label 1 ──[普通代码]──[挂起点2]──► label 2
label 2 ──[普通代码]──[return]

3.5 关键澄清:状态机是同步的,异步性来自挂起/恢复

这是最容易误解的一点!

协程的状态机代码本身是同步的 ------就是一个 switch + label。真正让它"异步"的是:

  1. 挂起时直接 return COROUTINE_SUSPENDED,把控制权交还调用者(线程被释放)
  2. delay / IO 完成后 ,由另一个线程或调度器 调用 resumeWith,重新进入这个函数

所以"异步"体现在 谁在什么时候再次调用 loadAll,而不是函数体本身。

delay 的等价实现(关键在于不阻塞线程):

java 复制代码
public static Object delay(long millis, Continuation<?> cont) {
    Dispatcher dispatcher = cont.getContext().get(Dispatcher.Key);
    dispatcher.scheduleAfter(millis, () -> {
        // ⚠️ 1 秒后,由调度器线程执行
        cont.resumeWith(Unit.INSTANCE);
    });
    return COROUTINE_SUSPENDED;   // 立即返回,当前线程释放
}

回调版对照(更直观地体现异步):

java 复制代码
// 协程状态机等价于这种"回调地狱"的拍平
public static void loadAllAsync(Callback<String> cb) {
    fetchUserAsync(userResult -> {
        fetchOrderAsync(orderResult -> {
            cb.onResult(userResult + " + " + orderResult);
        });
    });
}

一句话 :状态机函数体是同步的;异步性来自 COROUTINE_SUSPENDED 的 return 和之后 resumeWith 的重新进入。


第四部分:运行时------谁调用、怎么进入线程

4.1 runBlocking 做了什么

runBlocking 是普通函数(不是 suspend),负责:

  1. 创建事件循环 (BlockingEventLoop)
  2. 把它作为协程上下文传给 lambda
  3. 在当前线程 上立即启动 lambda
  4. 若 lambda 挂起,就在当前线程跑事件循环,直到完成
java 复制代码
public static <T> T runBlocking(Function1<Continuation<? super T>, Object> block) {
    BlockingEventLoop loop = new BlockingEventLoop();
    Continuation<T> completion = new Continuation<T>() {
        public void resumeWith(Object r) { loop.complete(r); }
        public CoroutineContext getContext() { return loop; }
    };

    Object r = block.invoke(completion);   // ★ 当前线程直接调用
    if (r != COROUTINE_SUSPENDED) return (T) r;

    return (T) loop.runUntilComplete();    // ★ 挂起了,跑事件循环
}

4.2 完整时序:loadAll 被进入三次

复制代码
═══════ 阶段 0:main 线程启动 ═══════
main 线程
  main() → runBlocking(block)
    └─ block.invoke(completion)
          └─ loadAll(c)                  ← ★ 第一次进入,main 线程
                case 0: fetchUser(c) → delay → 注册定时任务 → SUSPENDED
                return SUSPENDED
    └─ loop.runUntilComplete()           ← main 线程进入事件循环,park

═══════ 阶段 1:1 秒后,定时器触发 ═══════
定时器线程
  sleep(1000) 结束
  → loop.enqueue(() -> c.resumeWith(userResult))

═══════ 阶段 2:main 线程取出任务 ═══════
main 线程(还在 loop 里)
  task.run()
    └─ c.resumeWith(userResult)
          └─ loadAll(c)                  ← ★ 第二次进入,main 线程
                case 1: 取回 user, fetchOrder(c) → SUSPENDED
                return SUSPENDED
  └─ 继续 while,park

═══════ 阶段 3:再 1 秒后 ═══════
定时器线程:enqueue(resumeWith)
main 线程:
  task.run()
    └─ c.resumeWith(orderResult)
          └─ loadAll(c)                  ← ★ 第三次进入,main 线程
                case 2: 取回 order
                return "用户信息 + 订单信息"
          └─ completion.resumeWith(...)
                └─ loop.complete(...)
  └─ while 退出,runBlocking 返回

"谁调用了 loadAll":

第几次进入 谁调用 在哪个线程 走哪个 case
第 1 次 main lambda 的 invoke main case 0
第 2 次 LoadAllContinuation.resumeWith main case 1
第 3 次 LoadAllContinuation.resumeWith main case 2

"怎么进入线程":

  • 第一次 :main 线程同步调用链 runBlocking → block.invoke → loadAll
  • 挂起后 :loadAll return,栈弹出,main 线程跑事件循环
  • 恢复时 :定时器线程入队 任务,main 线程取出执行 ,重新进入 loadAll

runBlocking 场景下没有新线程参与业务逻辑,全程 main 线程,只是执行时机被切成三段。

4.3 结果如何向上传递

case 2 拿到 order 后,返回值同步 沿 resumeWith 链向上冒泡:

java 复制代码
// LoadAllContinuation.resumeWith
public void resumeWith(Object result) {
    this.result = result;
    Object r = ExampleKt.loadAll(this);      // 第三次进入,返回最终字符串
    if (r != COROUTINE_SUSPENDED) {
        this.completion.resumeWith(r);       // ★ 往外传
    }
}

// MainContinuation.resumeWith(main lambda)
public void resumeWith(Object result) {
    this.result = result;
    Object r = ExampleKt.main$lambda(this);  // 重新进入 main lambda
    if (r != COROUTINE_SUSPENDED) {
        runBlockingCompletion.resumeWith(r);
    }
}

// runBlockingCompletion.resumeWith
public void resumeWith(Object result) {
    loop.complete(result);                   // ★ 标记完成,唤醒 main 线程
}

完整链路:

复制代码
loadAll 返回 "用户信息 + 订单信息"
  → LoadAllContinuation.resumeWith 往外传
    → MainContinuation.resumeWith(println 打印)
      → runBlockingCompletion.resumeWith
        → loop.complete(...) 置 completed = true
          → runUntilComplete 的 while 退出
            → runBlocking 返回
              → main 线程解除阻塞,main 结束

第五部分:Dispatchers.IO 场景------多线程跳转

5.1 代码

kotlin 复制代码
suspend fun loadAll(): String = withContext(Dispatchers.IO) {
    val user = fetchUser()
    val order = fetchOrder()
    "$user + $order"
}

5.2 withContext 干了什么

  1. 切上下文 :把后续代码的 CoroutineContext 换成 IO
  2. 调度到 IO 线程池 :Dispatchers.IO 背后是共享线程池(默认最多 64 线程)
  3. 执行 block:在 IO 线程池的某个线程上跑
  4. 结果传回 :完成后 resumeWith 恢复回原上下文(这里是 main)

5.3 完整时序:跨三个 IO 线程

复制代码
═══════ 阶段 0:main 线程启动 ═══════
main 线程
  runBlocking → block.invoke → loadAll(c)
    └─ withContext(IO) { ... }
          → 把 block 调度到 IO 线程池
          → 返回 SUSPENDED               ← main 线程释放
  └─ runBlocking 进入事件循环,park

═══════ 阶段 1:IO 线程执行 block 开头 ═══════
IO-1 线程
  loadAll$block(c)                       ← ★ 第一次进入,IO-1
    case 0:
      fetchUser(c) → delay → SUSPENDED
  → IO-1 释放,回线程池

═══════ 阶段 2:1 秒后 ═══════
定时器线程
  → 投递 resumeWith 任务到 IO 线程池

IO-3 线程                              ← ★ 可能是不同线程!
  c.resumeWith(userResult)
    └─ loadAll$block(c)                  ← ★ 第二次进入,IO-3
          case 1:
            fetchOrder(c) → delay → SUSPENDED
  → IO-3 释放

═══════ 阶段 3:再 1 秒后 ═══════
IO-2 线程                              ← ★ 又是另一个线程!
  c.resumeWith(orderResult)
    └─ loadAll$block(c)                  ← ★ 第三次进入,IO-2
          case 2:
            return "用户信息 + 订单信息"

═══════ 阶段 4:切回 main ═══════
IO-2 线程
  → withContext 完成
  → 把结果 resumeWith 回原上下文(main 的 event loop)

main 线程
  completion.resumeWith("用户信息 + 订单信息")
    → main lambda 的 case 1:println(...)
    → runBlocking 退出

5.4 对比表

维度 runBlocking(无 withContext) withContext(Dispatchers.IO)
执行线程 始终 main IO 线程池任意线程
线程切换 无 每次挂起/恢复可能切换
并发能力 单线程事件循环 多线程并行
delay 恢复后 main 事件循环 IO 线程池
适合场景 测试、简单示例 阻塞 IO(文件、网络)

结论 :加了 Dispatchers.IO 后,loadAll 状态机不再固定在一个线程 ,三次进入可能落在三个不同的 DefaultDispatcher-worker-N 线程上;block 完成后再切回 main 打印。


第六部分:挂起点前后有普通代码会怎样

6.1 例子

kotlin 复制代码
suspend fun loadAll(): String {
    val prefix = "【"                    // ① 挂起前
    println("开始加载")                   // ② 挂起前
    val user = fetchUser()               // ③ 挂起点 1
    val mid = " | "                      // ④ 两个挂起之间
    println("user 已拿到")                // ⑤ 两个挂起之间
    val order = fetchOrder()             // ⑥ 挂起点 2
    val suffix = "】"                    // ⑦ 挂起后
    return prefix + user + mid + order + suffix   // ⑧
}

6.2 核心规则

编译器只在 suspend 调用点切 case;普通代码按"属于哪一段"被分配到相邻的 case 里。

切成 3 段:

段 label 包含代码
段 1 0 ①②③
段 2 1 ④⑤⑥
段 3 2 ⑦⑧

只有 3 个 case,不管中间多少行普通代码。

6.3 等价 Java(真实字节码形态)

真实产物是每个挂起点前一个小 switch:

java 复制代码
public static Object loadAll(Continuation<? super String> completion) {
    LoadAllContinuation c = new LoadAllContinuation(completion);
    Object result;

    switch (c.label) {
        case 0:
            c.prefix = "【";                      // ①
            System.out.println("开始加载");        // ②
            c.label = 1;
            result = fetchUser(c);                // ③
            if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
            break;
        case 1:
            result = c.result;                    // 取回 user
            break;
        default:
            throw new IllegalStateException();
    }

    String user = (String) result;
    c.user = user;

    c.mid = " | ";                                // ④
    System.out.println("user 已拿到");             // ⑤

    switch (c.label) {
        case 1:
            c.label = 2;
            result = fetchOrder(c);               // ⑥
            if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
            break;
        case 2:
            result = c.result;                    // 取回 order
            break;
        default:
            throw new IllegalStateException();
    }

    String order = (String) result;
    c.order = order;

    String suffix = "】";                          // ⑦
    return c.prefix + c.user + c.mid + c.order + suffix;   // ⑧
}

6.4 局部变量提升

java 复制代码
final class LoadAllContinuation implements Continuation<Object> {
    int label = 0;
    Object result;
    String prefix;    // ★ 跨挂起点
    String user;      // ★ 跨挂起点
    String mid;       // ★ 跨挂起点
    String order;     // ★ 跨挂起点
    // suffix 不提升:只在最后一段用,不跨挂起点
    final Continuation<? super String> completion;
}

6.5 各种位置代码归属

代码位置 归属 是否提升字段
fetchUser 之前 case 0 跨挂起点使用 → 提升
两个挂起之间 case 1 跨第二个挂起点使用 → 提升
fetchOrder 之后 case 2 一般不提升

纯语句 (如 println)不产生变量,不提升字段 ,直接留在对应 case 段。

6.6 try/catch 穿插

如果挂起点被包在 try 里,编译器会插入异常表 和额外的 label 分支,处理"从挂起点恢复时抛异常"的情况------这是 COROUTINE_SUSPENDED 之外的第二种返回路径 (返回 Result.failure)。

kotlin 复制代码
suspend fun loadAll(): String {
    val user = try {
        fetchUser()
    } catch (e: Exception) {
        "默认用户"
    }
    // ...
}

第七部分:总结与速查表

7.1 核心机制一句话

suspend 函数 = 带 Continuation 参数的函数;每个挂起点切一个 case(label);挂起时 return COROUTINE_SUSPENDED 保存状态并弹出栈;恢复时通过 resumeWith 重新进入函数,用 label 跳到对应 case 继续执行。

7.2 关键概念速查

概念 说明
suspend fun 编译后多一个 Continuation 参数,返回 Object
COROUTINE_SUSPENDED 特殊标记,表示真的挂起了
Continuation.resumeWith 恢复入口,重新进入状态机
label 状态机状态,决定从哪个 case 继续
局部变量提升 跨挂起点使用的变量提升为 Continuation 字段
runBlocking 阻塞当前线程跑事件循环,桥接阻塞/协程世界
withContext(IO) 切换调度器,代码在 IO 线程池执行

7.3 状态机规则

  • N 个挂起点 → N+1 个状态(label 0...N)→ 代码切成 N+1 段
  • 每个挂起点前一个小 switch
  • 普通代码不增加 case,只塞进相邻段
  • 跨挂起点变量提升为字段

7.4 线程模型对比

场景 执行线程 恢复后线程
runBlocking main main(事件循环)
withContext(Dispatchers.IO) IO 线程池 可能不同 IO 线程
Dispatchers.Main main main
Dispatchers.Default 计算线程池 可能不同线程

7.5 三个"反直觉"但重要的点

  1. 状态机代码是同步的,异步性来自"挂起返回 + resumeWith 重新进入"这个循环。
  2. runBlocking 下全程 main 线程,只是执行时机被切成多段,不是多线程。
  3. Dispatchers.IO 下同一状态机的多次进入可能在不同线程,由线程池调度决定。

7.6 常用并发写法

kotlin 复制代码
// 并发执行多个独立任务
suspend fun loadAll() = coroutineScope {
    val a = async { fetchA() }
    val b = async { fetchB() }
    "${a.await()} + ${b.await()}"
}

// 简化:awaitAll
suspend fun loadAllSimple() = coroutineScope {
    listOf(async { fetchA() }, async { fetchB() }).awaitAll()
}

// 异常独立处理
suspend fun loadAllSafe() = coroutineScope {
    val a = async { runCatching { fetchA() }.getOrDefault("失败") }
    val b = async { runCatching { fetchB() }.getOrDefault("失败") }
    "${a.await()} + ${b.await()}"
}

7.7 验证字节码

bash 复制代码
kotlinc -Xno-optimize Example.kt
javap -c -p ExampleKt.class

会看到:

  • loadAll 方法签名带 Continuation
  • 方法体是 tableswitch(对应 when(label))
  • if_acmpne 之类指令判 COROUTINE_SUSPENDED

附录:完整调用链示意

复制代码
Kotlin 源码
    │
    │ 编译(CPS + 状态机)
    ▼
Java 状态机(switch + label + Continuation)
    │
    │ 运行
    ▼
runBlocking 创建事件循环
    │
    ├─ 第一次进入(label 0)→ 遇到挂起点 → return SUSPENDED
    │                                     │
    │                          线程进入事件循环 park
    │                                     │
    ├─ 定时器/IO 完成 → enqueue(resumeWith)
    │                                     │
    ├─ 事件循环取出 → 第二次进入(label 1)→ 又挂起 → return SUSPENDED
    │                                     │
    ├─ 再次完成 → enqueue → 第三次进入(label 2)→ 真正 return
    │                                     │
    └─ 结果沿 resumeWith 链上冒泡 → loop.complete → runBlocking 返回

相关推荐
程序员清风1 小时前
双指针与滑动窗口:高效处理连续区间
开发语言·算法
代钦塔拉1 小时前
【C++/Qt 进阶】设计模式系列(2):工厂模式——从游戏角色到披萨店
c++·算法·设计模式
思茂信息1 小时前
电磁仿真学涉及哪些算法?
人工智能·算法·数学建模·matlab·emc·电磁仿真·仿真辐射
开开心心就好1 小时前
文件被占用删不掉?解锁工具强制删除
java·运维·服务器·开发语言·前端·pdf·ocr
weixin_460443562 小时前
Java+Linux私有化训考平台:服务、数据库与终端部署结构解析
java·linux·数据库·私有化部署·企业培训考试系统·企业考试系统
糖果豆豆2 小时前
让 AI 自查三遍,它只会把错的说得更像对的——检查的人和被检查的,是同一个
算法·架构
云和数据.ChenGuang2 小时前
python大小整数范围解析
java·开发语言·python
Java内核笔记2 小时前
Spring AI 生产级 Chat 接口:6 个开关、13 个坑,其中 2 个开关根本没生效
java·人工智能·后端
雷一言说2 小时前
Spring AI 2.0 RAG:用 Metadata Filter 做多租户检索隔离
java·spring