One of the biggest strengths of RxJava is its ability to easily schedule work and process results on various threads. This article aims to give you a solid foundation of working with threads in RxJava and RxAndroid to optimize system performance while avoiding bugs (threading-related bugs are notoriously hard to track down).

RxJava Schedulers

Threading in RxJava is done with help of Schedulers. Scheduler can be thought of as a thread pool managing 1 or more threads. Whenever a Scheduler needs to execute a task, it will take a thread from its pool and run the task in that thread.

Let’s summarize available Scheduler types and their common uses:

Schedulers.io() is backed by an unbounded thread pool. It is used for non CPU-intensive I/O type work including interaction with the file system, performing network calls, database interactions, etc. This thread pool is intended to be used for asynchronously performing blocking IO. Schedulers.computation() is backed by a bounded thread pool with size up to the number of available processors. It is used for computational or CPU-intensive work such as resizing images, processing large data sets, etc. Be careful: when you allocate more computation threads than available cores, performance will degrade due to context switching and thread creation overhead as threads vie for processors’ time. Schedulers.newThread() creates a new thread for each unit of work scheduled. This scheduler is expensive as new thread is spawned every time and no reuse happens. Schedulers.from(Executor executor) creates and returns a custom scheduler backed by the specified executor. To limit the number of simultaneous threads in the thread pool, use Scheduler.from(Executors.newFixedThreadPool(n)) . This guarantees that if a task is scheduled when all threads are occupied, it will be queued. The threads in the pool will exist until it is explicitly shutdown. Main thread or AndroidSchedulers.mainThread() is provided by the RxAndroid extension library to RxJava. Main thread (also known as UI thread) is where user interaction happens. Care should be taken not to overload this thread to prevent janky non-responsive UI or, worse, Application Not Responding” (ANR) dialog. Schedulers.single() is new in RxJava 2. This scheduler is backed by a single thread executing tasks sequentially in the order requested. Schedulers.trampoline() executes tasks in a FIFO (First In, First Out) manner by one of the participating worker threads. It’s often used when implementing recursion to avoid growing the call stack.

WARNING: Be careful writing multi-threaded code using unbounded thread Schedulers such as Schedulers.io() and Schedulers.newThread() . Depending on your data stream and the transformations you apply to it, it’s easier than you think to flood your system with threads.

Default threading in RxJava

If you don’t specify threading in RxJava (if you don’t specify subscribeOn , observeOn or both), the data will be emitted and processed by the current scheduler/thread (usually the main thread). For instance, all operators in the chain below will be processed by the current thread.

Note: some operators, such as interval , operate on a computation thread by default. See below for more details.

We can specify a thread to execute any operator by using subscribeOn and/or observeOn .

subscribeOn affects upstream operators (operators above the subscribeOn )

affects operators (operators above the ) observeOn affects downstream operators (operators below the observeOn )

affects operators (operators below the ) If only subscribeOn is specified, all operators will be be executed on that thread

is specified, all operators will be be executed on that thread If only observeOn is specified, all operators will be executed on the current thread and only operators below the observeOn will be switched to thread specified by the observeOn

For instance, in the following chain:

Data emission just and the map operator will be executed on the io scheduler as directed by the upstream operator subscribeOn .

filter will be executed on the computation scheduler as directed by the downstream operator observeOn .

Read on for more details, ways to debug as well as nuances of the threading operator in RxJava.

Debugging threading

As before, let’s look at a basic RxJava chain where we emit Strings and calculate their lengths.

When executed, this will print:

item length 4

item length 6

item length 7

Now, let’s see what thread this work is being done on by printing out thread info in doOnNext() , a side effect operator that gets executed for each item emitted.

When executed, this will print:

processing item on thread main

item length 4

processing item on thread main

item length 6

processing item on thread main

item length 7

So this stream is being emitted and processed on the main thread which makes sense because the block of code above resides inside the main method of my class.

Doing work on background thread

Often it makes sense to delegate certain work to a background thread. A typical example would be offloading an IO operation from the main thread. RxJava makes it easy.

subscribeOn() operator tells the source Observable which thread to emit and push items on all the way down to Observer (hence, it affects both upstream and downstream operators). It does not matter where you put the subscribeOn() in your Observable chain of operators.

Things to remember about our Observable are:

It is a “cold” Observable which means the emission occurs lazily only when Subscriber is added (call to subscribe() is made). New emission will happen for each subscriber added. subscribeOn() specifies a Scheduler (thread pool) where the work will be performed after subscription is made in subscribe() . The results of transformation are received on the same thread as the thread that did the actual work. This can be changed using observeOn() as we’ll see soon.

Let’s run the updated code example inside the main method.

Using subscribeOn()

When executed, this will print nothing!

This is because the main method finished executing before the background thread returned results. To get around this, let’s keep the main method alive for an additional 3 seconds with Thread.sleep(3000) — long enough to give our Observable a chance to fire emissions on the background thread.

When executed, this will print:

processing item on thread RxNewThreadScheduler-1

item length 4 received on RxNewThreadScheduler-1

processing item on thread RxNewThreadScheduler-1

item length 6 received on RxNewThreadScheduler-1

processing item on thread RxNewThreadScheduler-1

item length 7 received on RxNewThreadScheduler-1

The results of the background thread work are returned on the same thread, RxNewThreadScheduler-1.

When performing Network/IO/computation tasks, using background scheduler is crucial. Without subscribeOn() , your code will use a caller thread to perform operations, causing Observable to become blocking.

Understanding observeOn()

As we saw above, subscribeOn() instructs the source Observable which thread to emit items on — this thread will push the emissions all the way to our Observer . However, if it encounters an observeOn() anywhere in the chain, it will switch and pass emissions using that Scheduler for the remaining (downstream) operations.

Usually the observing thread in Android is the main (UI) thread, AndroidSchedulers.mainThread() . This requires RxAndroid extension library to RxJava.

Let’s modify our example code to perform background work on Schedulers.newThread() but then switch to AndroidSchedulers.mainThread() .

Using observeOn()

When executed, we will see that now results are received by the main thread.

processing item on thread RxNewThreadScheduler-1

processing item on thread RxNewThreadScheduler-1

processing item on thread RxNewThreadScheduler-1

item length 4 received on thread main

item length 6 received on thread main

item length 7 received on thread main

Doing work asynchronously

While RxJava is known as a library for composing asynchronous and event-based programs using observable sequences, there are a plenty of useful tasks it can do synchronously. For instance, map(String::length) above handles each item using the same thread RxNewThreadScheduler-1 sequentially preserving the same order.