10.1 RxJava Subject:与 Sinks 的对照

对比 RxJava 3 的五种 Subject(PublishSubject、BehaviorSubject、ReplaySubject、AsyncSubject、UnicastSubject)与 Reactor Sinks 的语义映射。实测 late subscriber 在 PublishSubject 下丢失历史、BehaviorSubject 补最新值、AsyncSubject 只在完成时发射,并用并发探针证明裸 Subject 的 onNext 不会串行化(maxConcurrent=2),必须 toSerialized()(maxConcurrent=1)。示例基于 RxJava 3.1.12 与 reactor-core 3.8.8。

Subject 是什么

RxJava 的 Subject 同时是 Observer 和 Observable:既能接收上游信号,也能把信号转发给下游。这和 Reactor 的 Sinks.Many 定位相同,但两者的并发契约不同,后面会单独讲。

RxJava 3 提供五种 Subject:

Subject缓存策略后订阅者订阅者数
PublishSubject不缓存丢失订阅前的元素多个
BehaviorSubject最新 1 个立即收到当前值多个
ReplaySubject全部或有界收到历史多个
AsyncSubject最后 1 个完成时才收到最后值多个
UnicastSubject有界队列只允许一个订阅者1 个

PublishSubject:只转发新事件

import io.reactivex.rxjava3.subjects.PublishSubject;

import java.util.ArrayList;
import java.util.List;

PublishSubject<Integer> subject = PublishSubject.create();
subject.onNext(1);                       // 还没有订阅者

List<Integer> late = new ArrayList<>();
subject.subscribe(late::add);

subject.onNext(2);
subject.onComplete();

System.out.println("late=" + late);

实测输出:

late=[2]

1 在订阅前发射,直接丢失;后订阅者只看到 2 和完成信号。这与 Reactor 的 Sinks.many().multicast().directBestEffort() 最接近。

BehaviorSubject:总有「当前值」

BehaviorSubject.createDefault(v) 要求一个初始值;create() 则允许在首个值到来前为空。

BehaviorSubject<Integer> subject = BehaviorSubject.createDefault(0);
subject.onNext(1);

List<Integer> late = new ArrayList<>();
subject.subscribe(late::add);
subject.onNext(2);

System.out.println("late=" + late);      // [1, 2]

后订阅者先收到当前值 1,再收到之后发射的 2。如果使用 create() 且尚未发射任何值,后订阅者什么也收不到(实测输出为空列表),直到第一个 onNext 到达。

Reactor 的对应写法是 Sinks.many().replay().limit(1);需要初始值时用 replay().latestOrDefault(v)。区别在于 BehaviorSubject 总是有「当前值」这个概念,而 replay sink 的历史可以为空。

ReplaySubject:按数量或时间重放

ReplaySubject<Integer> subject = ReplaySubject.create();   // 无界
subject.onNext(1);
subject.onNext(2);

List<Integer> late = new ArrayList<>();
subject.subscribe(late::add);
subject.onNext(3);

System.out.println("late=" + late);      // [1, 2, 3]

ReplaySubject.createWithSize(2) 只保留最近 2 个,实测发射 1、2、3 后订阅:

replay size 2: [2, 3]

还有 createWithTime(maxAge, unit) 和 createWithTimeAndSize(...),分别按时间和双重上界保留历史。Reactor 对应 Sinks.many().replay().all()、limit(n)、limit(Duration)。

注意 ReplaySubject.create() 是无界的:对无限流使用它,内存会随元素数量持续增长。

AsyncSubject:完成时才发射最后一个值

AsyncSubject 的行为容易想当然。它在 onComplete() 之前不向下游发射任何元素,只记住最后一个值;onComplete() 时才把这个值发出去:

AsyncSubject<Integer> subject = AsyncSubject.create();

List<Integer> values = new ArrayList<>();
subject.subscribe(values::add);

subject.onNext(1);
subject.onNext(2);
System.out.println("before complete=" + values);   // []

subject.onComplete();
System.out.println("after complete=" + values);    // [2]

实测:

before complete=[]
after complete=[2]

完成后才订阅的观察者同样只收到 2。如果 onComplete() 前一个值都没有,它会正常完成但没有元素:

async empty: values=[] completed=true

Reactor 没有完全等价的 Sinks。Sinks.one() 也是单值 sink,但它在 tryEmitValue(v) 时就发射并终止,不等待「完成」这个额外信号。需要「完成时才取最后值」的语义,可以用 replay().latest() 配合 asFlux().last():last() 会等源完成后再发出最后一个元素。

UnicastSubject:单订阅者 + 队列

UnicastSubject 只允许一个观察者,并且在观察者到来之前把元素缓冲起来:

UnicastSubject<Integer> subject = UnicastSubject.create();
subject.onNext(1);
subject.onNext(2);

List<Integer> late = new ArrayList<>();
subject.subscribe(late::add);
subject.onNext(3);

System.out.println("late=" + late);      // [1, 2, 3]

第二个订阅者会在 onError 收到 IllegalStateException: Only a single observer allowed.。Reactor 对应 Sinks.many().unicast().onBackpressureBuffer(),零订阅者时同样接受发射并缓冲。

并发发射:Subject 不会替你串行化

一个常见的说法是「Subject 是线程安全的,可以从任意线程 onNext」。更准确的说法是:Subject 的订阅/取消是线程安全的,但 onNext 信号本身不会自动串行化。

用一个会睡眠的观察者做探针:两个线程各发射 10 次,观察者记录同时进入 onNext 的最大数量。

AtomicInteger inside = new AtomicInteger();
AtomicInteger maxConcurrent = new AtomicInteger();

subject.subscribe(new Observer<Integer>() {
    @Override public void onSubscribe(Disposable d) { }

    @Override public void onNext(Integer v) {
        int now = inside.incrementAndGet();
        maxConcurrent.accumulateAndGet(now, Math::max);
        try { Thread.sleep(5); } catch (InterruptedException e) { }
        inside.decrementAndGet();
    }

    @Override public void onError(Throwable e) { }
    @Override public void onComplete() { }
});

Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) subject.onNext(i); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 10; i++) subject.onNext(i); });
t1.start(); t2.start();
t1.join(); t2.join();

实测:

裸 PublishSubject:        maxConcurrentOnNext=2
PublishSubject.toSerialized(): maxConcurrentOnNext=1

裸 Subject 确实让同一个观察者的 onNext 并发执行了,这违反 Reactive Streams 的串行信号规则。修复方式是 subject.toSerialized():它把所有发射串行化到内部队列,代价是每次发射多一层同步与排队。

这条规则对 BehaviorSubject、ReplaySubject、AsyncSubject 同样适用:需要多线程发射时,一律先 toSerialized()。

Reactor 的选择不同:Sinks.Many 默认检测并发发射并返回 FAIL_NON_SERIALIZED(见 9.1),不会静默地让两个线程同时进入下游。它也没有 toSerialized(),调用方必须自己保证串行,或者使用 Sinks.unsafe() 并自行承担后果。

Subject 与 Sinks 的映射

RxJavaReactor语义说明
PublishSubjectSinks.many().multicast().directBestEffort()不缓存,只转发新元素
PublishSubject + 缓冲Sinks.many().multicast().onBackpressureBuffer(n)多播 + 有界缓冲
BehaviorSubjectSinks.many().replay().limit(1)后订阅者补最新值
BehaviorSubject.createDefault(v)Sinks.many().replay().latestOrDefault(v)带初始值
ReplaySubject.create()Sinks.many().replay().all()无界历史
ReplaySubject.createWithSize(n)Sinks.many().replay().limit(n)有界历史
ReplaySubject.createWithTime(...)Sinks.many().replay().limit(Duration)按时间保留
AsyncSubject无完全等价;Sinks.one() 近似Sinks.one() 发射即完成,不等待显式完成
UnicastSubjectSinks.many().unicast().onBackpressureBuffer()单订阅者队列

订阅者泄漏

Subject 会持有当前所有观察者的引用,直到 Disposable.dispose() 把它们移除。长生命周期的 Subject(单例事件总线、静态状态容器)配上忘记释放的短生命周期订阅者,就会持续累积:

// 每个请求都订阅,但从不释放
for (Request r : requests) {
    subject.subscribe(event -> handle(r, event));   // Disposable 被丢弃
}

这些 handle 闭包连同 r 一起被 Subject 持有,请求对象无法回收。修复不是「少订阅几次」,而是把 Disposable 纳入生命周期管理:

CompositeDisposable perRequest = new CompositeDisposable();
perRequest.add(subject.subscribe(event -> handle(r, event)));

// 请求结束时
perRequest.dispose();

Reactor 的 Flux.subscribe() 返回的 Disposable 同理;Sinks 本身也不会自动清理订阅者,它只负责在取消时把它们从数组里移除。

选择建议

条件建议
Android 项目,已有 RxJava 依赖继续用 Subject,注意 toSerialized()
Spring WebFlux / 服务端用 Sinks,FAIL_NON_SERIALIZED 比静默并发更早暴露问题
需要 AsyncSubject 的「完成时取最后值」RxJava 直接用;Reactor 用 replay().latest() + last()
需要单订阅者队列RxJava UnicastSubject;Reactor Sinks.many().unicast()
多线程发射RxJava toSerialized();Reactor 自己串行化

两个库的能力高度重叠,真正的差异在失败模型:RxJava Subject 在并发发射时没有统一的结果反馈,Reactor Sinks 用 EmitResult 把「没发出去」变成可检查的返回值。

小结

关注点结论
PublishSubject不缓存,后订阅者丢失历史
BehaviorSubject后订阅者立即收到当前值;无值且未完成时为空
ReplaySubject可无界、按数量或按时间保留历史
AsyncSubject完成时才发射最后一个值;空完成则无元素
UnicastSubject只允许一个观察者,订阅前缓冲
并发裸 Subject 不串行化 onNext,必须 toSerialized()

阅读导航

上一篇:9.3 Sinks 重放:大小与时间限制

下一篇:10.2 ConnectableObservable 与 connect

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南