【WebFlux】第八篇 —— 自定义调度器

Flux / Mono 自定义线程调度

线程调度主要讲(Schedulers / publishOn / subscribeOn

前置:你已经学过 handlegeneratecreatebufferlimitRatedoOnXXXonXxxSinks

本文聚焦「响应式代码跑在哪些线程上」------这是把响应式程序从「能跑」变成「跑得对、跑得稳」的分水岭。


1. 一句话定位:Scheduler 是什么

Reactor 默认在调用线程 上同步执行。一旦要「异步」「并行」「把阻塞 IO 隔离」,就必须把部分链路交给一个 Scheduler

Scheduler = 响应式世界里的「线程池抽象」。它不直接暴露 Thread,而是提供 Worker(一个可调度任务的执行单元)。你通过 publishOn(scheduler) / subscribeOn(scheduler) 把链路贴到某个调度器上。

关键认知:Reactor 不会自动帮你切线程。你不调 publishOn/subscribeOn,一切都还在当前线程(通常是 main 或 Netty EventLoop)上跑------在 EventLoop 上 sleep/阻塞会直接拖垮整个服务。

2. 内置 Schedulers 速查表

调度器 线程模型 适合场景 备注
Schedulers.immediate() 当前线程(不切) 调试、想「就在这线程跑」 不是新线程
Schedulers.single() 全局单条 Worker 线程 需要严格串行化的任务 进程内共享单例
Schedulers.parallel() 固定 N=CPU核数 条线程 CPU 密集型计算 默认并行度=Runtime.getRuntime().availableProcessors()
Schedulers.boundedElastic() 弹性线程池(有上限) 阻塞 IO(DB/HTTP/文件) 默认上限 10×核数线程 + 10万任务队列,防 OOM
Schedulers.fromExecutor(Executor) 用你给的线程池 接入既有线程池 自定义调度器入口
Schedulers.fromExecutorService(ExecutorService) 同上,可关闭 接入既有线程池 同上
Schedulers.newParallel(name, n) 新建独立 parallel 池 隔离某类计算 dispose() 释放
Schedulers.newBoundedElastic(...) 新建独立弹性池 隔离某类 IO dispose() 释放
Schedulers.newSingle(name) 新建独立单线程池 隔离某类串行任务 dispose() 释放

黄金法则

  • CPU 密集 / 纯计算 → parallel(别在它上面阻塞)。
  • 阻塞 IO / 等待外部 → boundedElastic(别在它上面跑重计算,浪费线程)。
  • 不同业务线要隔离线程池(避免 A 业务 IO 拥塞把 B 业务拖死)→ 自定义调度器。

3. 自定义调度器(本文重点)

3.1 基于普通线程池:Schedulers.fromExecutorService

最常用场景:接入项目里已有的 ExecutorService,或给线程池命名(排查问题时一眼看出是哪条业务线)。

3.2 基于 Reactor 自带的 newXxx

当你需要独立、可控、用完可释放的池子时用它们(而不是污染全局共享池)。

3.3 命名线程工厂(排查利器)

默认线程名是 parallel-1boundedElastic-3 这种,看不出业务。自定义 ThreadFactory 给线程加前缀,线上排查线程 dump 时能直接定位。


4. publishOn vs subscribeOn(位置语义,最重要)

这是全文最容易被搞混的点,务必结合第 5 节的样例亲手跑一遍

4.1 publishOn(scheduler) ------ 改变「它之后」的线程

  • publishOn 作用在**它下游(代码顺序中它下方)**的所有操作符,直到遇到下一个 publishOn
  • 每遇到一个 publishOn,后续链路就被切到那个调度器。
  • 可多次叠加,每加一个就再切一次。

4.2 subscribeOn(scheduler) ------ 决定「订阅与源」发生在哪

  • subscribeOn 决定订阅动作本身 + 上游源的产生在哪个线程上发生。
  • 它影响的是「链的起点」,对「下游已经被 publishOn 接管的处理线程」没有影响。
  • 多次 subscribeOn 时,最靠近 source(上游)的那一个生效(代码里最靠上的那个)。

记忆法:

  • publishOn从它往下换线程(下游视角)。
  • subscribeOn从它往上(含源)换线程(上游视角)。

5. 完整可运行样例

样例 1:内置调度器线程名一览

java 复制代码
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;

public class BuiltinSchedulers {
    public static void main(String[] args) throws InterruptedException {
        show("immediate", Schedulers.immediate());
        show("single", Schedulers.single());
        show("boundedElastic", Schedulers.boundedElastic());
        show("parallel", Schedulers.parallel());
        Thread.sleep(300);
    }

    static void show(String name, reactor.core.scheduler.Scheduler s) {
        Flux.range(1, 1)
            .publishOn(s)
            .doOnNext(i -> System.out.println(
                name + " -> 线程: " + Thread.currentThread().getName()))
            .blockLast();
    }
}

预期输出(顺序可能因调度略有差异,线程名前缀稳定):

复制代码
immediate -> 线程: main
single -> 线程: single-1
boundedElastic -> 线程: boundedElastic-1
parallel -> 线程: parallel-1

注意 immediate 在当前 main 线程------它开新线程,只是「就在调用线程跑」。


样例 2:位置语义最直观对照

java 复制代码
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;

public class PublishOnVsSubscribeOn {
    static int log(String tag, int i) {
        System.out.println(tag + " @" + Thread.currentThread().getName() + " 值=" + i);
        return i * 10;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== publishOn:逐个切换下游线程 ===");
        Flux.range(1, 3)
            .map(i -> log("map1(源端)", i))           // 订阅线程:main
            .publishOn(Schedulers.parallel())
            .map(i -> log("map2(parallel后)", i))     // parallel 线程
            .publishOn(Schedulers.boundedElastic())
            .map(i -> log("map3(elastic后)", i))      // boundedElastic 线程
            .blockLast();

        System.out.println("\n=== subscribeOn:决定订阅/源发生的线程 ===");
        Flux.range(1, 3)
            .subscribeOn(Schedulers.parallel())       // 订阅+源都搬到 parallel
            .map(i -> log("A(parallel订阅)", i))       // parallel 线程
            .map(i -> log("B", i))                     // 仍在 parallel
            .blockLast();

        System.out.println("\n=== 多个 subscribeOn:最靠近 source 的生效 ===");
        Flux.range(1, 3)
            .subscribeOn(Schedulers.parallel())       // 最靠近 source
            .map(i -> log("X", i))                     // 在 parallel
            .subscribeOn(Schedulers.boundedElastic()) // 下游,被上游覆盖
            .blockLast();

        Thread.sleep(200);
    }
}

预期输出

复制代码
=== publishOn:逐个切换下游线程 ===
map1(源端) @main 值=1
map1(源端) @main 值=2
map1(源端) @main 值=3
map2(parallel后) @parallel-1 值=10
map2(parallel后) @parallel-1 值=20
map2(parallel后) @parallel-1 值=30
map3(elastic后) @boundedElastic-1 值=100
map3(elastic后) @boundedElastic-1 值=200
map3(elastic后) @boundedElastic-1 值=300

=== subscribeOn:决定订阅/源发生的线程 ===
A(parallel订阅) @parallel-1 值=1
A(parallel订阅) @parallel-1 值=2
A(parallel订阅) @parallel-1 值=3
B @parallel-1 值=10
B @parallel-1 值=20
B @parallel-1 值=30

=== 多个 subscribeOn:最靠近 source 的生效 ===
X @parallel-1 值=1
X @parallel-1 值=2
X @parallel-1 值=3

关键观察:最后的 X 跑在 parallel 而非 boundedElastic------证明「多个 subscribeOn,最靠近 source 的赢」。


样例 3:自定义命名线程池 + newBoundedElastic

java 复制代码
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import reactor.core.scheduler.Scheduler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomScheduler {
    public static void main(String[] args) throws InterruptedException {
        // 1) 自定义命名固定线程池(接入既有池 / 排查友好)
        ThreadFactory tf = new ThreadFactory() {
            private final AtomicInteger n = new AtomicInteger(1);
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "biz-pool-" + n.getAndIncrement());
                t.setDaemon(true);
                return t;
            }
        };
        ExecutorService pool = Executors.newFixedThreadPool(4, tf);
        Scheduler customPool = Schedulers.fromExecutorService(pool);

        System.out.println("=== 自定义固定线程池(fromExecutorService)===");
        Flux.range(1, 6)
            .publishOn(customPool)
            .doOnNext(i -> System.out.println(
                "线程: " + Thread.currentThread().getName() + " 值=" + i))
            .blockLast();
        pool.shutdown();   // 用普通线程池记得关掉

        // 2) Reactor 自带 newBoundedElastic(带上限、TTL、命名,用完 dispose)
        Scheduler elastic = Schedulers.newBoundedElastic(
                8,        // 最多 8 个线程
                1000,     // 任务队列上限 1000
                "my-elastic",
                60,       // 空闲 60s 回收
                false);
        System.out.println("\n=== newBoundedElastic ===");
        Flux.range(1, 4)
            .publishOn(elastic)
            .doOnNext(i -> System.out.println(
                "线程: " + Thread.currentThread().getName() + " 值=" + i))
            .blockLast();
        elastic.dispose();   // 释放,避免线程泄漏

        Thread.sleep(200);
    }
}

预期输出

复制代码
=== 自定义固定线程池(fromExecutorService)===
线程: biz-pool-1 值=1
线程: biz-pool-2 值=2
线程: biz-pool-3 值=3
线程: biz-pool-4 值=4
线程: biz-pool-1 值=5
线程: biz-pool-2 值=6

=== newBoundedElastic ===
线程: my-elastic-1 值=1
线程: my-elastic-2 值=2
线程: my-elastic-3 值=3
线程: my-elastic-4 值=4

线程名前缀 biz-pool- / my-elastic- 是由你的 ThreadFactory / name 参数决定的------线上排查一眼可定位业务线。


样例 4:实战组合:IO 用弹性池、计算用并行池

java 复制代码
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

public class IoThenCompute {
    static String blockingIo(int id) {
        try { Thread.sleep(200); } catch (InterruptedException ignored) {}
        return "io-result-" + id;
    }
    static int compute(String s) { return s.hashCode(); }

    public static void main(String[] args) {
        String result = Mono.fromCallable(() -> blockingIo(1))
            .subscribeOn(Schedulers.boundedElastic())  // ① 阻塞 IO 放进弹性池
            .map(IoThenCompute::compute)               // ② 仍在弹性池(IO 刚结束)
            .publishOn(Schedulers.parallel())          // ③ 切到并行池做 CPU 计算
            .map(h -> "hash=" + h)
            .block();
        System.out.println("done -> " + result);
    }
}

预期输出

复制代码
done -> hash=逻辑值

这条链是 WebFlux / R2DBC 业务里最经典的写法:① 把阻塞调用用 subscribeOn(boundedElastic) 搬离 EventLoop;③ 计算阶段 publishOn(parallel) 利用多核。


6. 常见坑

  1. 在 EventLoop / main 上阻塞 :响应式框架(WebFlux)的 IO 线程是 EventLoop,上面 sleep/DB 调用会拖垮所有请求。任何阻塞都要 subscribeOn(boundedElastic)
  2. parallel 上做阻塞 IOparallel 线程数=CPU 核数,被阻塞就再无线程做计算了 → 全服务卡死。阻塞一律用 boundedElastic
  3. boundedElastic 上跑重计算 :弹性池线程多,跑纯计算既浪费又可能和其他 IO 争抢。计算用 parallel
  4. 每次请求都 newBoundedElastic/newParallel :会不断新建线程池、泄漏线程。应声明为 static final 常量复用 ,或明确 dispose() 释放。
  5. publishOn / subscribeOn 位置写反 :想切「处理线程」却写了 subscribeOn(它只管源);想切「源线程」却写了 publishOn。记住 4.1 / 4.2 的位置语义。
  6. 以为 subscribeOn 能多线程并行发元素subscribeOn 只是把订阅搬到单条线程,不会把一个 Flux 并行化处理(那要用 parallel() 操作符或 flatMapconcurrency)。

7. 速查表(收藏级)

你想做 用哪个
纯计算、并行处理 Schedulers.parallel() + publishOn
阻塞 IO(DB/HTTP/文件) Schedulers.boundedElastic() + subscribeOn
严格串行某类任务 Schedulers.single() / newSingle(name)
接入已有线程池 Schedulers.fromExecutorService(pool)
给业务线隔离独立池 Schedulers.newBoundedElastic(...) / newParallel(...)
切换「下游处理」线程 publishOn(scheduler)
切换「源/订阅」线程 subscribeOn(scheduler)
给线程命名便于排查 自定义 ThreadFactoryname 参数

8. 所以呢?

  1. 调度不是「锦上添花」,是「正确性」 :在响应式框架里,阻塞放错线程 = 生产事故。parallel 算、boundedElastic 堵,是铁律。
  2. 自定义调度器用于「隔离 + 可排查」 :给线程池命名、给不同业务线独立池,线上出问题看线程 dump 能秒定位。语法就是 Schedulers.fromExecutorService(pool)Schedulers.newBoundedElastic(...)
  3. publishOn 管下游、subscribeOn 管源:位置一错,看到的现象完全两样------务必跑一遍第 5 节样例,把「线程名」印在脑子里。
相关推荐
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第210题】【10_网络协议篇】第1题:说说 TCP/IP 网络五层模型
java·网络协议·tcp/ip·计算机网络·网络五层模型
wuminyu1 小时前
JUC组件逐层剥离与深度剖析
java·linux·c语言·jvm·c++·算法
大黄说说1 小时前
EF Core 避坑指南:查询慢、循环查询、并发更新问题如何解决
java·服务器·数据库
城管不管1 小时前
重生——第五次面试2026.8.1一面
java·数据库·后端·ai·面试·职场和发展·agent
DarLing丶张皇1 小时前
【源码】JeecgBoot导出Excel模板
java·spring boot
223糖2 小时前
Idea,pycharm2026激活保姆级教程
java·ide·intellij-idea
geovindu2 小时前
java: Gale-Shapley Algorithm
java·开发语言·后端·算法
LUCKY-LIVING2 小时前
ELF File in linux
android·java·linux
星核0penstarry2 小时前
DeepSeek-V4-Flash 正式公测:大模型行业进入「极速平价普惠时代」
java·开发语言·人工智能