bigywater.tech
Spring · 57

publishOn 与 subscribeOn:线程在哪里切换

subscribeOn 和 publishOn 到底有什么区别?写在链的哪里有关系吗?多次调用谁生效?阻塞的老代码该放在哪个调度器?为什么 flatMap 里要加 subscribeOn 才并行?这篇在 Reactor 3.8.7 上把每个阶段的线程名打印出来。

310 → 2 ms
阻塞源不加 / 加 subscribeOn(boundedElastic) 时,subscribe() 这一行的耗时
1235 → 304 ms
flatMap 里 4 个阻塞 300 ms 的内层,不加 / 加 subscribeOn 的总耗时
10
200 个任务放到 parallel 调度器上实际用到的线程数(等于核数)

线程在哪里切换

实测 · 本机执行输出
没有任何调度器                                              订阅发生在:main → 源发出:main → 订阅者收到:main
subscribeOn(boundedElastic)(放在链的末尾)                  订阅发生在:boundedElastic-1 → 源发出:boundedElastic-1 → map 之后:boundedElastic-1 → 订阅者收到:boundedElastic-1
publishOn(parallel) 放在源与 map 之间                      订阅发生在:main → 源发出:main → publishOn 之后:parallel-1 → 订阅者收到:parallel-1
subscribeOn(boundedElastic) + publishOn(parallel)    订阅发生在:boundedElastic-1 → 源发出:boundedElastic-1 → publishOn 之后:parallel-2 → 订阅者收到:parallel-2
subscribeOn(single) 再 subscribeOn(boundedElastic)    订阅发生在:single-1 → 源发出:single-1 → 订阅者收到:single-1
publishOn(single) 再 publishOn(boundedElastic)        订阅发生在:main → 源发出:main → 第一个 publishOn 之后:single-1 → 第二个 publishOn 之后:boundedElastic-1 → 订阅者收到:boundedElastic-1

subscribeOn 影响订阅与源,publishOn 影响它之后的阶段。

异步源的线程

实测 · 本机执行输出
Mono.delay(50ms) 之后的下游:[delay 完成后的 doOnNext:parallel-3]
在线程 my-completer 里 complete 一个 CompletableFuture:[fromFuture 完成后的 doOnNext:my-completer]

下游线程由上游来源决定。

阻塞的源

实测 · 本机执行输出
阻塞源(300 ms),不加 subscribeOn:subscribe() 调用本身用了 310 ms(卡住了调用线程)
阻塞源(300 ms),subscribeOn(boundedElastic):subscribe() 调用用了 2 ms,整体完成用了 305 ms

subscribeOn 决定谁来被阻塞,不会让阻塞变快。

并行

实测 · 本机执行输出
Flux.range(1, 6).publishOn(parallel).map:[1@parallel-4, 2@parallel-4, 3@parallel-4, 4@parallel-4, 5@parallel-4, 6@parallel-4]
flatMap 里 4 个阻塞 300 ms 的内层,内层不加调度器:总耗时 1235 ms,线程分布 [1@main, 2@main, 3@main, 4@main]
flatMap 里 4 个阻塞 300 ms 的内层,内层加 subscribeOn(boundedElastic):总耗时 304 ms,线程分布 [1@boundedElastic-1, 2@boundedElastic-2, 3@boundedElastic-3, 4@boundedElastic-4]

flatMap 加内层 subscribeOn 才并行。

调度器

实测 · 本机执行输出
availableProcessors = 10;200 个阻塞 20 ms 的任务放到 Schedulers.parallel() 上,一共只用了 10 个线程:[parallel-1, parallel-10, parallel-2, parallel-3, parallel-4, parallel-5, parallel-6, parallel-7, parallel-8, parallel-9]
40 个并发阻塞任务在 boundedElastic 上用了 40 个线程,例如 [boundedElastic-1, boundedElastic-10, boundedElastic-11]

parallel 的线程数等于核数,阻塞操作去 boundedElastic。

分步演示

线程切换
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 reactor-core 3.8.7 的单个 Java 程序,不依赖 Spring 容器;机器为 10 个逻辑核心;线程名 parallel-N、boundedElastic-N 的序号随运行顺序变化,只有线程类别可复现
    • 没有验证:Schedulers.newParallel 与自定义调度器;Schedulers.boundedElastic 的上限(默认线程数上限与任务队列上限)被触及时的行为;虚拟线程调度器(见第 60 篇);Hooks.onScheduleHook;publishOn 的 prefetch 对线程切换次数的影响(见第 56 篇);WebFlux 中 Netty event loop 与这些调度器的关系(见第 58 篇)
    京ICP备2026067502号-1京公网安备11010502063487号