线程调度与 Schedulers:Project Reactor 并发模型的核心引擎

Project Reactor 是Java响应式编程库 ,提供MonoFlux 核心类型,支持非阻塞、背压及异步数据流处理 。它是Spring WebFlux 的基础,适用于构建高并发低延迟的微服务与事件驱动应用,遵循Reactive Streams规范。

1. 响应式编程入门:从阻塞困境到数据流之美 2. 深入 Project Reactor:从原理到工程实践的全面指南 3. Flux 与 Mono:Project Reactor 核心响应式类型深度解析 4. Mono:Project Reactor 中最精巧的响应式原语 5. 创建 Flux/Mono 并订阅:Project Reactor 响应式编程的第一步 6. 程序化创建响应式序列:Flux.generate、Flux.create 与 Flux.push 深度解析 7. 线程调度与 Schedulers:Project Reactor 并发模型的核心引擎 8. 响应式流中的错误处理:Project Reactor 异常治理全体系 9. Sinks API:Project Reactor 中程序化发射数据的现代方案

引言

响应式编程的核心承诺之一是非阻塞 ------用少量线程处理海量并发连接。但"非阻塞"不是魔法,它需要一套精密的线程调度机制 来支撑。在 Project Reactor 中,这套机制的核心抽象就是 Scheduler

默认情况下,Flux 和 Mono 的操作符链在当前线程上执行。如果你需要将部分工作切换到特定线程池(如 I/O 线程池、CPU 计算线程池),就需要通过 subscribeOn() 和 publishOn() 配合 Scheduler 来实现。

本文将系统梳理 Reactor 的调度器体系:从内置 Scheduler 的语义与线程模型,到 subscribeOn 与 publishOn 的精确行为差异,再到工程实践中的调度策略与常见陷阱。

一、Scheduler 接口:调度器的抽象

1.1 核心定义

java 复制代码
public interface Scheduler extends Disposable {
    Disposable schedule(Runnable task);
    Disposable schedule(Runnable task, long delay, TimeUnit unit);
    Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit);
    Worker createWorker();
    void start();
    void dispose();
    boolean isDisposed();
    long now(TimeUnit unit);
}

Scheduler 本质上是一个任务提交与执行的抽象层------它屏蔽了底层线程池的具体实现(ExecutorService、ForkJoinPool、单线程、ScheduledExecutorService 等),向 Reactor 操作符提供统一的 schedule(Runnable) 接口。

1.2 Worker:调度器的工作单元

java 复制代码
public interface Worker extends Disposable {
    Disposable schedule(Runnable task);
    Disposable schedule(Runnable task, long delay, TimeUnit unit);
    Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit);
    void dispose();
}

Worker 代表从 Scheduler 中获取的一个执行上下文。同一个 Worker 上的任务保证顺序执行(FIFO),不同 Worker 之间可能并行。

二、内置 Scheduler 全景

Reactor 通过 Schedulers 工具类提供了多种开箱即用的调度器:

2.1 速查表

Scheduler 线程模型 线程数 适用场景 是否共享
Schedulers.immediate() 当前线程 0(不创建新线程) 测试、无切换需求 ---
Schedulers.single() 共享单线程 1 低延迟串行任务、定时器 ✅ 全局共享
Schedulers.parallel() 固定线程池 CPU 核数 CPU 密集型计算 ✅ 全局共享
Schedulers.boundedElastic() 弹性有界线程池 10 × CPU 核数(上限) I/O 阻塞操作 ✅ 全局共享
Schedulers.fromExecutor(executor) 自定义 Executor 取决于 Executor 桥接已有线程池 ❌ 每次新建
Schedulers.fromExecutorService(es) 自定义 ExecutorService 取决于 ES 精细控制线程池 ❌ 每次新建
Schedulers.elastic() 弹性无界线程池 无上限 已废弃 ---

2.2 Schedulers.immediate()

java 复制代码
Scheduler immediate = Schedulers.immediate();
  • 不创建新线程,任务在当前调用线程上立即执行
  • 相当于"无调度"
  • 主要用于测试或明确不需要线程切换的场景

2.3 Schedulers.single()

java 复制代码
Scheduler single = Schedulers.single();
  • 所有任务在同一个共享线程上顺序执行
  • 适合:低延迟定时任务、顺序事件处理、需要串行保证的场景
  • ⚠️ 如果某个任务阻塞,后续所有任务都会被阻塞
  • 变体:Schedulers.newSingle("name") 创建独立的新单线程调度器
java 复制代码
// 使用场景:确保事件按序处理
Flux<Event> events = eventSource.getEvents()
    .publishOn(Schedulers.single())
    .subscribe(event -> processSequentially(event));

2.4 Schedulers.parallel()

java 复制代码
Scheduler parallel = Schedulers.parallel();
  • 固定大小线程池,线程数 = Runtime.getRuntime().availableProcessors()(CPU 核数)
  • 适合:CPU 密集型任务(数据变换、计算、编解码)
  • ⚠️ 绝对不要在此线程池上执行阻塞操作------线程数有限,阻塞会导致线程饥饿
  • 变体:Schedulers.newParallel("name", n) 创建自定义大小的并行调度器
java 复制代码
// CPU 密集型计算
Flux.range(1, 1000)
    .parallel()
    .runOn(Schedulers.parallel())
    .map(i -> expensiveComputation(i))
    .sequential()
    .subscribe();

2.5 Schedulers.boundedElastic()(重点)

java 复制代码
Scheduler boundedElastic = Schedulers.boundedElastic();

这是 Reactor 3.3+ 引入的调度器,替代了已废弃的 Schedulers.elastic()。 核心特性:

参数 默认值 说明
最大线程数 10 × CPU 核数 可配置上限
最大排队任务数 100,000 / 线程 超出则拒绝
空闲线程存活时间 60 秒 超时后回收
线程创建策略 按需创建 负载增加时扩展

设计意图:

为阻塞 I/O 操作提供一个有上限的弹性线程池,避免无限创建线程导致 OOM。

java 复制代码
// ✅ 正确:将阻塞 JDBC 调用切换到 boundedElastic
Mono<User> user = Mono.fromCallable(() -> {
    // 阻塞操作
    return jdbcTemplate.queryForObject(
        "SELECT * FROM users WHERE id = ?", new UserMapper(), id
    );
}).subscribeOn(Schedulers.boundedElastic());

与已废弃的 elastic() 对比:

维度 elastic()(已废弃) boundedElastic()
线程上限 ❌ 无限制 ✅ 10 × CPU 核数
任务队列 无界 有界(100K/线程)
OOM 风险
推荐状态 ❌ 废弃 ✅ 推荐

2.6 自定义 Scheduler

java 复制代码
// 从已有 Executor 创建
ExecutorService myPool = Executors.newFixedThreadPool(4);
Scheduler customScheduler = Schedulers.fromExecutorService(myPool);

// 从已有 Executor 创建(无法 dispose)
Executor executor = ForkJoinPool.commonPool();
Scheduler forkJoinScheduler = Schedulers.fromExecutor(executor);

// 使用完毕后释放
customScheduler.dispose();

2.7 JDK 21+ 虚拟线程调度器

java 复制代码
// Spring Framework 6.1+ / Reactor 3.6+ 支持
Scheduler virtualThreadScheduler = Schedulers.fromExecutorService(
    Executors.newVirtualThreadPerTaskExecutor()
);

Mono<User> user = Mono.fromCallable(() -> blockingService.getUser(id))
    .subscribeOn(virtualThreadScheduler);

三、subscribeOn vs publishOn:两大调度操作符

这是 Reactor 线程调度中最核心、也最易混淆的两个操作符。

3.1 subscribeOn(Scheduler)

影响整个订阅过程和数据源的发射线程。

java 复制代码
Flux<Integer> flux = Flux.range(1, 5)
    .map(i -> {
        System.out.println("map: " + Thread.currentThread().getName());
        return i * 2;
    })
    .subscribeOn(Schedulers.parallel());  // 整个链在 parallel 线程池执行

flux.subscribe(i -> System.out.println("subscribe: " + Thread.currentThread().getName()));

关键行为:

  • 无论 subscribeOn 放在链的哪个位置,效果相同(因为它影响的是订阅信号,而订阅信号是从下游向上游传播的)
  • 影响数据源的发射线程
  • 影响其上方所有操作符的执行线程
  • 如果链中有多个 subscribeOn,最靠近源头的(最上面的)生效
java 复制代码
Flux.range(1, 5)
    .map(i -> i)           // ← 在 parallel 线程执行
    .subscribeOn(Schedulers.parallel())   // 影响源头和上方
    .map(i -> i)           // ← 也在 parallel 线程执行(除非被 publishOn 改变)
    .subscribeOn(Schedulers.boundedElastic())  // ❌ 无效!第一个 subscribeOn 已生效
    .subscribe();

3.2 publishOn(Scheduler)

影响其下方操作符的执行线程(数据信号的切换点)。

java 复制代码
Flux.range(1, 5)
    .map(i -> {
        System.out.println("Before publishOn: " + Thread.currentThread().getName());
        return i;
    })
    .publishOn(Schedulers.parallel())  // 从这里开始切换线程
    .map(i -> {
        System.out.println("After publishOn: " + Thread.currentThread().getName());
        return i * 2;
    })
    .subscribe(i -> System.out.println("Subscribe: " + Thread.currentThread().getName()));

关键行为:

  • 位置敏感------只影响其下方的操作符
  • 可以出现多次,每次切换都生效
  • 影响的是数据信号(onNext/onError/onComplete) 的执行线程
  • 不影响数据源的发射线程

3.3 核心对比

维度 subscribeOn publishOn
影响范围 整个订阅过程(源头 + 上方) 仅下方操作符
位置敏感性 ❌ 不敏感(放哪都一样) ✅ 敏感(只影响下方)
多次调用 只有最靠近源头的生效 每次都生效
影响的信号 订阅信号 + 数据发射 仅数据信号
典型用途 切换数据源/阻塞操作线程 切换下游处理线程

3.4 组合使用的经典模式

java 复制代码
Mono.fromCallable(() -> {
    // ① 阻塞 I/O:在 boundedElastic 线程池执行
    return blockingDatabase.query(id);
})
.subscribeOn(Schedulers.boundedElastic())  // ② 源头切到弹性线程池
.map(data -> {
    // ③ 仍在 boundedElastic 线程
    return transform(data);
})
.publishOn(Schedulers.parallel())          // ④ 从这里开始切到 parallel
.map(data -> {
    // ⑤ CPU 密集计算:在 parallel 线程池执行
    return expensiveCompute(data);
})
.subscribe(result -> {
    // ⑥ 仍在 parallel 线程
    System.out.println(result);
});

线程切换时序图:

文本 复制代码
Thread: boundedElastic-1     Thread: parallel-1
─────────────────────────    ─────────────────────
① blockingDatabase.query()
② subscribe 信号传播
③ transform(data)
                             ④ publishOn 切换点
                             ⑤ expensiveCompute(data)
                             ⑥ subscribe(result)

3.5 多次 publishOn 切换

java 复制代码
Flux.range(1, 3)
    .map(i -> "A" + i)                          // Thread: main
    .publishOn(Schedulers.parallel())
    .map(s -> "B" + s)                          // Thread: parallel-1
    .publishOn(Schedulers.boundedElastic())
    .map(s -> "C" + s)                          // Thread: boundedElastic-1
    .subscribe(s -> System.out.println(s));     // Thread: boundedElastic-1

四、调度器与操作符的交互

4.1 哪些操作符自带调度器?

某些操作符内部默认使用特定 Scheduler:

操作符 默认 Scheduler 说明
Flux.interval(Duration) Schedulers.parallel() 定时发射
Mono.delay(Duration) Schedulers.parallel() 延迟发射
Flux.timeout(Duration) Schedulers.parallel() 超时检测
delayElements(Duration) Schedulers.parallel() 元素延迟
delaySubscription(Duration) Schedulers.parallel() 订阅延迟
java 复制代码
// interval 默认在 parallel 线程池
Flux.interval(Duration.ofSeconds(1))
    .subscribe(i -> System.out.println(Thread.currentThread().getName()));
// 输出: parallel-1

// 可以覆盖默认调度器
Flux.interval(Duration.ofSeconds(1), Schedulers.single())
    .subscribe(i -> System.out.println(Thread.currentThread().getName()));
// 输出: single-1

4.2 flatMap 中的并发与调度

java 复制代码
// flatMap 默认并发度 256,内部元素可能在 parallel 线程执行
Flux.range(1, 100)
    .flatMap(i -> Mono.fromCallable(() -> blockingCall(i))
                      .subscribeOn(Schedulers.boundedElastic()),
             32)  // 限制并发度为 32
    .subscribe();

五、工程实战场景

5.1 WebFlux 中隔离阻塞操作

java 复制代码
@Service
public class LegacyService {

    // ❌ 错误:直接在 EventLoop 线程执行阻塞调用
    public Mono<User> getUserBad(Long id) {
        return Mono.just(jdbcTemplate.queryForObject(...));  // 阻塞 EventLoop!
    }

    // ✅ 正确:切换到 boundedElastic
    public Mono<User> getUserGood(Long id) {
        return Mono.fromCallable(() ->
            jdbcTemplate.queryForObject(
                "SELECT * FROM users WHERE id = ?",
                new UserRowMapper(), id
            )
        ).subscribeOn(Schedulers.boundedElastic());
    }
}

5.2 CPU 密集型计算与 I/O 分离

java 复制代码
public Mono<Report> generateReport(Long userId) {
    return Mono.fromCallable(() -> fetchDataFromLegacyApi(userId))  // I/O
        .subscribeOn(Schedulers.boundedElastic())                    // I/O 线程
        .map(rawData -> parseAndValidate(rawData))                   // 仍在 I/O 线程
        .publishOn(Schedulers.parallel())                            // 切换到 CPU 线程
        .map(data -> computeStatistics(data))                        // CPU 密集计算
        .map(stats -> formatReport(stats));                          // 仍在 CPU 线程
}

5.3 并行处理 + 结果聚合

java 复制代码
public Mono<Dashboard> loadDashboard(Long userId) {
    return Mono.zip(
        userService.getUser(userId)
            .subscribeOn(Schedulers.boundedElastic()),
        orderService.getRecentOrders(userId)
            .subscribeOn(Schedulers.boundedElastic()),
        analyticsService.computeMetrics(userId)
            .subscribeOn(Schedulers.parallel())
    ).map(tuple -> new Dashboard(tuple.getT1(), tuple.getT2(), tuple.getT3()));
}

5.4 定时任务与调度

java 复制代码
// 每秒轮询一次
Flux.interval(Duration.ofSeconds(1))
    .flatMap(tick -> Mono.fromCallable(() -> pollExternalService())
                         .subscribeOn(Schedulers.boundedElastic()))
    .subscribe(data -> processNewData(data));

// 延迟执行
Mono.delay(Duration.ofMinutes(5))
    .then(cleanupTask())
    .subscribe();

// 定期执行
Flux.interval(Duration.ofHours(1), Schedulers.parallel())
    .flatMap(tick -> scheduledJob())
    .subscribe();
  

5.5 测试中控制调度器

java 复制代码
@Test
void testWithVirtualTime() {
    // 使用 VirtualTimeScheduler 避免真实等待
    StepVerifier.withVirtualTime(() ->
        Mono.delay(Duration.ofHours(2)).then(Mono.just("done"))
    )
    .expectSubscription()
    .expectNoEvent(Duration.ofHours(2))
    .expectNext("done")
    .verifyComplete();
}

@Test
void testWithImmediateScheduler() {
    // 使用 immediate 确保在当前线程执行(便于断言)
    Mono<String> result = Mono.fromCallable(() -> "hello")
        .subscribeOn(Schedulers.immediate());

    StepVerifier.create(result)
        .expectNext("hello")
        .verifyComplete();
}

六、Scheduler 的生命周期管理

6.1 共享 Scheduler 的复用

java 复制代码
// 全局共享,不需要手动 dispose
Scheduler shared = Schedulers.parallel();       // 全局单例
Scheduler shared2 = Schedulers.boundedElastic(); // 全局单例

多次调用 Schedulers.parallel() 返回的是同一个实例。

6.2 自定义 Scheduler 的释放

java 复制代码
// 自定义调度器需要手动管理生命周期
Scheduler custom = Schedulers.newParallel("my-pool", 4);

try {
    Flux.range(1, 100)
        .parallel()
        .runOn(custom)
        .map(i -> compute(i))
        .sequential()
        .blockLast();
} finally {
    custom.dispose();  // ⚠️ 必须释放!否则线程泄漏
}

6.3 Spring 容器中的管理

java 复制代码
@Configuration
public class SchedulerConfig {

    @Bean(destroyMethod = "dispose")
    public Scheduler ioScheduler() {
        return Schedulers.newBoundedElastic(
            16,             // 最大线程数
            1000,           // 每线程最大排队数
            "io-pool"       // 线程名前缀
        );
    }

    @Bean(destroyMethod = "dispose")
    public Scheduler cpuScheduler() {
        return Schedulers.newParallel("cpu-pool", 4);
    }
}

七、深入理解:线程切换的底层机制

7.1 publishOn 的内部实现

publishOn 本质上是在操作符链中插入了一个异步队列:

文本 复制代码
上游线程 → [Queue] → 下游线程
           ↑
     publishOn 在这里
     将 onNext 信号放入队列
     下游 Worker 从队列中取出并处理

这意味着:

  • publishOn 引入了一次线程上下文切换
  • 存在微小的延迟(队列的入队/出队)
  • 对于高频小数据,过多的 publishOn 会影响性能

7.2 subscribeOn 的传播机制

java 复制代码
Flux.range(1, 5)                    // source
    .map(...)                        // op1
    .subscribeOn(Schedulers.parallel())  // 调度点
    .filter(...)                     // op2
    .subscribe();                    // terminal

执行流程:

文本 复制代码
1. subscribe() 在 main 线程被调用
2. 订阅信号向上传播:terminal → op2 → [subscribeOn 拦截]
3. subscribeOn 将后续订阅操作提交到 parallel 线程池
4. 在 parallel 线程中:继续向上订阅 → op1 → source
5. source 在 parallel 线程中开始发射数据
6. 数据沿链向下流动,默认仍在 parallel 线程

八、常见陷阱与最佳实践

8.1 ❌ 在 EventLoop/parallel 线程中执行阻塞操作

java 复制代码
// ❌ 灾难性错误!阻塞了 EventLoop 线程
Flux.range(1, 10)
    .map(i -> {
        Thread.sleep(1000);  // 阻塞 parallel 线程!
        return i;
    })
    .subscribeOn(Schedulers.parallel())
    .subscribe();

后果:parallel 线程池只有 CPU 核数个线程,一个阻塞就少一个可用线程,最终导致线程饥饿。

java 复制代码
// ✅ 正确:阻塞操作必须在 boundedElastic
Flux.range(1, 10)
    .flatMap(i -> Mono.fromCallable(() -> {
        Thread.sleep(1000);  // 阻塞在弹性线程池,安全
        return i;
    }).subscribeOn(Schedulers.boundedElastic()))
    .subscribe();

8.2 ❌ 过多的 publishOn 切换

java 复制代码
// ❌ 性能杀手:频繁线程切换
Flux.range(1, 1000000)
    .publishOn(Schedulers.parallel())
    .map(i -> i + 1)
    .publishOn(Schedulers.boundedElastic())
    .map(i -> i * 2)
    .publishOn(Schedulers.parallel())
    .map(i -> i - 1)
    .subscribe();
java 复制代码
// ✅ 正确:只在必要时切换
Flux.range(1, 1000000)
    .publishOn(Schedulers.parallel())
    .map(i -> i + 1)
    .map(i -> i * 2)
    .map(i -> i - 1)
    .subscribe();

8.3 ❌ subscribeOn 位置误解

java 复制代码
// 以下两种写法效果完全相同:
Flux.range(1, 5).map(...).subscribeOn(Schedulers.parallel()).subscribe();
Flux.range(1, 5).subscribeOn(Schedulers.parallel()).map(...).subscribe();
// subscribeOn 的位置不影响效果!

8.4 ❌ 忽略 boundedElastic 的容量限制

java 复制代码
// ⚠️ boundedElastic 有上限:10 × CPU 核数
// 在 8 核机器上,最多 80 个线程
// 如果同时有 1000 个阻塞任务,大部分会排队等待

// 解决方案:限制并发度
Flux.fromIterable(hugeList)
    .flatMap(item -> blockingCall(item)
        .subscribeOn(Schedulers.boundedElastic()),
        50)  // 限制最多 50 个并发
    .subscribe();

8.5 ✅ 最佳实践清单

# 实践 说明
1 阻塞 I/O → boundedElastic 永远不要在 parallel/EventLoop 上阻塞
2 CPU 密集 → parallel 线程数 = CPU 核数,避免过度并行
3 串行保序 → single 需要严格顺序执行时
4 最少化 publishOn 每次切换都有上下文切换开销
5 subscribeOn 只写一次 多次调用只有第一个生效
6 自定义 Scheduler 必须 dispose 防止线程泄漏
7 用 flatMap 的 concurrency 参数限流 避免压垮 boundedElastic
8 测试用 VirtualTimeScheduler 避免真实等待
9 生产环境用共享 Scheduler 避免频繁创建/销毁线程池
10 JDK 21+ 考虑虚拟线程 Executors.newVirtualThreadPerTaskExecutor()

九、调度策略选择决策树

文本 复制代码
你的任务是什么类型?
│
├── 阻塞 I/O(JDBC、文件、同步 HTTP)?
│   └── → Schedulers.boundedElastic()
│         + subscribeOn()
│         + 限制并发度(flatMap concurrency)
│
├── CPU 密集计算(编解码、数据变换)?
│   └── → Schedulers.parallel()
│         + publishOn() 或 parallel().runOn()
│
├── 需要严格串行执行?
│   └── → Schedulers.single()
│         + publishOn()
│
├── 定时/延迟任务?
│   └── → 默认 parallel(Flux.interval)
│         或自定义 ScheduledExecutor
│
├── 桥接已有线程池?
│   └── → Schedulers.fromExecutorService(yourPool)
│
├── JDK 21+ 虚拟线程?
│   └── → Schedulers.fromExecutorService(
│             Executors.newVirtualThreadPerTaskExecutor())
│
└── 不需要切换线程?
    └── → 不调用 subscribeOn/publishOn(默认当前线程)

十、监控与调优

10.1 线程命名

java 复制代码
// 自定义线程名前缀,方便日志排查
Scheduler ioScheduler = Schedulers.newBoundedElastic(16, 1000, "my-io");
Scheduler cpuScheduler = Schedulers.newParallel("my-cpu", 4);

// 日志输出示例:
// [my-io-1] Executing database query...
// [my-cpu-3] Computing statistics...

10.2 线程池监控

java 复制代码
// 通过 Micrometer 暴露 Scheduler 指标
Schedulers.onScheduleHook("metrics", runnable -> {
    // 包装任务以记录指标
    return () -> {
        Timer.Sample sample = Timer.start(registry);
        try {
            runnable.run();
        } finally {
            sample.stop(registry.timer("scheduler.task"));
        }
    };
});

10.3 全局钩子

java 复制代码
// 全局调度钩子:在所有任务调度时执行
Schedulers.onScheduleHook("logging", runnable -> () -> {
    System.out.println("Task scheduled on: " + Thread.currentThread().getName());
    runnable.run();
});

// 移除钩子
Schedulers.resetOnScheduleHook("logging");

十一、与 Spring WebFlux 的集成

11.1 Netty EventLoop 线程

Spring WebFlux 默认运行在 Netty EventLoop 线程上(类似 parallel,线程数 = CPU 核数)。

java 复制代码
// 在 Controller 中,当前线程就是 Netty EventLoop
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
    // ⚠️ 这里运行在 EventLoop 线程上
    // 绝对不能执行阻塞操作!
    return userRepository.findById(id);  // R2DBC:非阻塞,安全
}

11.2 混合阻塞与非阻塞

java 复制代码
@GetMapping("/report/{id}")
public Mono<Report> getReport(@PathVariable Long id) {
    return reportService.generateAsync(id)           // 非阻塞 R2DBC
        .flatMap(data -> Mono.fromCallable(() ->
            legacyPdfGenerator.createPdf(data)       // 阻塞 PDF 生成
        ).subscribeOn(Schedulers.boundedElastic()))  // 切到弹性线程池
        .publishOn(Schedulers.parallel())            // 切回 CPU 线程
        .map(pdf -> compressPdf(pdf));               // CPU 密集压缩
}

十二、总结

文本 复制代码
┌──────────────────────────────────────────────────────────────────────┐
│                    Reactor Scheduler 核心知识图谱                      │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  Scheduler 类型:                                                     │
│  ┌─────────────────┬────────────────────────────────────────┐        │
│  │ immediate       │ 当前线程,无切换                          │       │
│  │ single          │ 共享单线程,串行                          │       │
│  │ parallel        │ 固定线程池(CPU核数),CPU密集              │       │
│  │ boundedElastic  │ 弹性有界线程池(10×CPU),I/O阻塞           │       │
│  │ fromExecutor    │ 自定义线程池桥接                          │       │
│  └─────────────────┴────────────────────────────────────────┘       │
│                                                                     │
│  调度操作符:                                                         │
│  ┌──────────────────────────────────────────────────────────┐       │
│  │ subscribeOn(Scheduler)                                   │       │
│  │ • 影响源头 + 上方所有操作符                                  │       │
│  │ • 位置不敏感                                               │       │
│  │ • 只有最靠近源头的生效                                       │       │
│  │ • 用途:切换数据源/阻塞操作线程                               │       │
│  ├──────────────────────────────────────────────────────────┤       │
│  │ publishOn(Scheduler)                                     │       │
│  │ • 只影响下方操作符                                          │       │
│  │ • 位置敏感                                                │       │
│  │ • 可多次调用                                               │       │
│  │ • 用途:切换下游处理线程                                     │       │
│  └──────────────────────────────────────────────────────────┘       │
│                                                                      │
│  黄金法则:                                                           │  
│  • 阻塞 → boundedElastic + subscribeOn                               │
│  • CPU密集 → parallel + publishOn                                    │
│  • 不要在 EventLoop/parallel 上阻塞                                   │
│  • 最小化线程切换次数                                                   │
│  • 自定义 Scheduler 必须 dispose                                      │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Scheduler 是 Reactor 并发模型的基石。理解每种调度器的线程模型、掌握 subscribeOn 与 publishOn 的精确语义、遵循"阻塞隔离"原则,是构建高性能响应式应用的必备技能。

在微服务架构中,一个请求可能跨越数据库查询(boundedElastic)、数据变换(parallel)、消息推送(single)等多个调度域。正确的调度策略,就是让这些异构操作各得其所、互不干扰的编排艺术。

相关推荐
ClouGence1 小时前
当 AI 开始直接操作数据库,传统数据库管理工具还有必要吗?
数据库·后端·agent
lazy H1 小时前
不同的消息队列有什么区别?Kafka、RabbitMQ、RocketMQ、Pulsar、ActiveMQ 选型对比
后端·中间件·kafka·rabbitmq·rocketmq
明月_清风1 小时前
开发者写PPT自救指南:4类对接场景,把技术讲清楚
前端·后端·面试
明月_清风1 小时前
从经典self-Attention 到 Flash Attention:为什么我们「不必算出每一个 âᵢ」
后端·ai编程
m0_640602441 小时前
2026 年餐饮收银系统前后端技术实现——核心架构与原理详解
后端·微服务·云原生·架构
Scene2161 小时前
Flux 与 Mono:Project Reactor 核心响应式类型深度解析
后端
lingran__2 小时前
C++ STL unordered系列(哈希) 底层剖析与模拟实现万字详解 | 基于哈希表,复刻 SGI-STL 泛型哈希容器架构
开发语言·c++·后端·哈希算法·哈希表·泛型编程·unordered系列
小林ixn2 小时前
NestJS 入门实战:从 0 到 1 撸一个 Todo CRUD,感受装饰器与模块化的优雅
后端·mvc·nestjs
阿弱2 小时前
graph-core 的边与命令模式设计
java·后端·agent