RxJava操作符observeOn的并发问题
前几天同事丢给我一段RxJava的代码,代码想要通过observeOn
指定线程池来实现多线程消费,这个想法本身很美好。实际却没有发现多线程消费的现象,所以我决定好好研究一番,就以?的代码开始吧!
long sleepTime = 300;
long emitFrequency = 100;
Observable.interval(emitFrequency, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.from(Executors.newFixedThreadPool(4)))
.subscribe(aLong -> {
System.out.println(aLong + " -> " + Thread.currentThread());
sleep(sleepTime);
});
output:
0 -> Thread[pool-2-thread-1,5,main]
1 -> Thread[pool-2-thread-1,5,main]
2 -> Thread[pool-2-thread-1,5,main]
3 -> Thread[pool-2-thread-1,5,main]
4 -> Thread[pool-2-thread-1,5,main]
5 -> Thread[pool-2-thread-1,5,main]
6 -> Thread[pool-2-thread-1,5,main]
7 -> Thread[pool-2-thread-1,5,main]
8 -> Thread[pool-2-thread-1,5,main]
9 -> Thread[pool-2-thread-1,5,main]
操作符基础
首先,我们要先搞清楚,observeOn
做了什么,通过debug一路进到observeOn
操作符真正做事情的地方ObservableObserveOn
,其主要作用是
- 对接上游(Upstream),通过
source.subscribe(...)
通知上游订阅开始 - 消费上游消息,这里
ObserveOnObserver
RxJava中的操作符本质上基于责任链模式,语法层面的链式调用也正是基于此,从?代码可以清晰的看出这种链式结构。其中FooObserver
中的actual
是下游的观察者,FooObserver
本身通过source.subscribe
被注册到上游,当上游调用onNext
,FooObserver
收到后往往下游调用。
@Override
protected void subscribeActual(Observer<? super T> observer) {
source.subscribe(new FooObserver(observer, ...));
}
static final class FooObserver implements Observer<T> {
final Observer<? super T> actual;
FooObserver(Observer<? super T> actual) {
this.actual = actual;
}
public void onSubscribe(Disposable s) {
...
actual.onSubscribe(s);
}
public void onNext(T t) {
actual.onNext(t);
}
...
}
分析
既然预期多线程消费,那么从上面我们知道消息总是从onNext
进来,ObservableObserveOn
的onNext
方法:
if (sourceMode != QueueDisposable.ASYNC) {
queue.offer(t);
}
schedule();
在本例中,sourceMode != QueueDisposable.ASYNC
总是true,可见每次有值进来先放到queue
里面。再看schedule
void schedule() {
if (getAndIncrement() == 0) {
worker.schedule(this);
}
}
ObserveOnObserver
继承了AtomicInteger
,那么它自身是个计数器,当getAndIncrement() == 0
时,放到线程池中运行,这里有个问题计数器用来干什么?我们继续看run
方法的实现,里面直接调用了drainNormal
,大意乃是排干的意思,我们不是有queue
么,只要有我就消费,见?代码:
void drainNormal() {
int missed = 1;
final SimpleQueue<T> q = queue;
final Observer<? super T> a = downstream;
for (;;) {
// check termination
for (;;) {
boolean d = done;
T v;
try {
v = q.poll();
} catch (Throwable ex) {
// handling exception
return;
}
// check termination
a.onNext(v);
}
missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
这段代码给我们的启发在于:内部的for循环总是去queue
里面取值,有就直接运行;如果没有就,跳出循环了,并且把计数器减去missed
,这里的missed
是指未处理的数据,外面的for循环保证missed == 0
后退出,所以只有当missed == 0
的时候,schedule
中才会调用worker.schedule(this)
,开辟新的线程来运行;否则会在一个线程里面先把队列中的消息消费完。读者不妨把一开始的sleepTime
调小一点,这样就能观察到多线程消费了。
为什么这么设计?
这是因为Observable的协议的定义里面,建议我们onNext
总是串行调用,不要运行在多于一个线程上面。为什么呢?我也在思考这个问题,欢迎高手解答。
Observable Contract which states that onNext() must be called sequentially and never concurrently by more than one thread at a time.
解决办法
对于observeOn
,现在只有消费速度足够快,消费端才能做到并发消费。同事给出的理由是:正因为消费慢才希望做到并发消费。这个问题也是有办法的。这里提供两种思路:
引入线程池
既然Rx协议不建议并发消费,那我们自己定制线程池,可控性好,可以根据生产速率定制线程数,队列长度等。
final ExecutorService es = Executors.newFixedThreadPool(4);
Observable.interval(emitFrequency, TimeUnit.MILLISECONDS)
.subscribe(aLong -> {
es.submit(() -> {
// 耗时操作
});
});