Reactive programming is an approach to writing software that embraces asynchronous I/O. Asynchronous I/O is a small idea that portends big changes for software. The idea is simple: alleviate inefficient resource utilization by reclaiming resources that would otherwise be idle as they waited for I/O activity. Asynchronous I/O inverts the normal design of I/O processing: the clients are notified of new data instead of asking for it; this frees the client to do other things while waiting for new notifications. Let’s look at an example that compares and contrasts asynchronous I/O to synchronous I/O.

Let’s build a simple program that reads data from a source (a java.io.File reference, specifically). First up, an implementation that uses a trusty 'ol java.io.InputStream implementation:

Example 1. Read data from a file synchronously package com.example.io ; import lombok.extern.log4j.Log4j2 ; import org.springframework.util.FileCopyUtils ; import java.io.File ; import java.io.FileInputStream ; import java.io.IOException ; import java.util.function.Consumer ; @Log4j2 class Synchronous implements Reader { @Override public void read ( File file , Consumer < BytesPayload > consumer ) throws IOException { try ( FileInputStream in = new FileInputStream ( file )) { (1) byte [] data = new byte [ FileCopyUtils . BUFFER_SIZE ]; int res ; while (( res = in . read ( data , 0 , data . length )) != - 1 ) { (2) consumer . accept ( BytesPayload . from ( data , res )); (3) } } } } 1 source the file using a regular java.io.File 2 pull the results out of the source one line at a time…​ 3 I’ve written this code to accept a Consumer<BytesPayload> that gets called when there’s new data

Pretty straightforward, eh? Run this and you’ll see in the log output, on the left-hand side of each line, that all activity is happening on a single thread.

We’re pulling bytes out of a source of data (in this case, a java.io.InputStream subclass, java.io.FileInputStream ). What’s wrong with this example? Well, probably nothing! In this case, we’re using an InputStream that’s pointing to data on the local file system. If the file is there, and the hard drive is working, then this code will work as we expect.

What if, instead of reading data from a File , we read data from a network socket, and used a different implementation of an InputStream ? Nothing to worry about! Well, nothing to worry about if the network is infinitely fast, at least. And if the network link between this node and another never fails. If those things are true, then there’s certainly nothing to worry about! This code will work just fine.

What happens if the network is slow, or down? In this case, it’d mean that the time it takes for the in.read(…​) operation to return would be prolonged. Indeed, it may never return! This is a problem if we’re trying to do something else with the thread on which we’re reading data. Sure, we can spin up another thread and read from that one instead. We could keep this up to a point, but eventually, we’ll run into a limit where adding threads doesn’t support our goal of scaling. We won’t have true concurrency beyond the number of cores on our machine. We’re stuck! We can’t handle more I/O, reads in this case, without adding threads, and our ability to scale up with more threads is, ultimately, limited.

In that example, the bulk of the work is in the reading - there’s not much else going on anywhere. We are _I/O bound. Let’s see how an asynchronous solution can help us alleviate the monopolization of our threads.

Example 2. Read data from a file asynchronously package com.example.io ; import lombok.extern.log4j.Log4j2 ; import org.springframework.util.FileCopyUtils ; import java.io.File ; import java.io.IOException ; import java.nio.ByteBuffer ; import java.nio.channels.AsynchronousFileChannel ; import java.nio.channels.CompletionHandler ; import java.nio.file.Path ; import java.nio.file.StandardOpenOption ; import java.util.Collections ; import java.util.concurrent.ExecutorService ; import java.util.concurrent.Executors ; import java.util.function.Consumer ; @Log4j2 class Asynchronous implements Reader , CompletionHandler < Integer , ByteBuffer > { private int bytesRead ; private long position ; private AsynchronousFileChannel fileChannel ; private Consumer < BytesPayload > consumer ; private final ExecutorService executorService = Executors . newFixedThreadPool ( 10 ); public void read ( File file , Consumer < BytesPayload > c ) throws IOException { this . consumer = c ; Path path = file . toPath (); (1) this . fileChannel = AsynchronousFileChannel . open ( path , Collections . singleton ( StandardOpenOption . READ ), this . executorService ); (2) ByteBuffer buffer = ByteBuffer . allocate ( FileCopyUtils . BUFFER_SIZE ); this . fileChannel . read ( buffer , position , buffer , this ); (3) while ( this . bytesRead > 0 ) { this . position = this . position + this . bytesRead ; this . fileChannel . read ( buffer , this . position , buffer , this ); } } @Override public void completed ( Integer result , ByteBuffer buffer ) { (4) this . bytesRead = result ; if ( this . bytesRead < 0 ) return ; buffer . flip (); byte [] data = new byte [ buffer . limit ()]; buffer . get ( data ); (5) consumer . accept ( BytesPayload . from ( data , data . length )); buffer . clear (); this . position = this . position + this . bytesRead ; this . fileChannel . read ( buffer , this . position , buffer , this ); } @Override public void failed ( Throwable exc , ByteBuffer attachment ) { log . error ( exc ); } } 1 this time, we adapt the java.io.File into a Java NIO java.nio.file.Path 2 when we create the Channel , we specify, among other things, a java.util.concurrent.ExecutorService , that will be used to invoke our CompletionHandler when there’s data available 3 start reading, passing in a reference to a CompletionHandler<Integer, ByteBuffer> ( this ) 4 in the callback, we read the bytes out of a ByteBuffer into a byte[] holder 5 just as in the Synchronous example, the byte[] data is passed to a consumer

First thing’s first: this code’s waaaay more complicated! There’s a ton of things going on here and it can seem overwhelming, but indulge me, for a moment…​ This code reads data from a Java NIO Channel and processes that data, asynchronously, on a separate thread in a callback handler. The thread on which the read was started isn’t monopolized. We return virtually instantly after we call .read(..) , and when there is finally data available, our callback is invoked, and on a different thread. If there is latency between .read() calls, then we can move on and do other things with our thread. The duration of the asynchronous read, from the first byte to the last, is at best as short as the duration of the synchronous read. It’s likely a tiny bit longer. But, for that complexity, we can be more efficient with our threads. We can handle more work, multiplexing I/O across a finite thread pool.

I work for a cloud computing company. We’d love it if you solved your scale-out problems by buying more application instances! Of course, I’m being a bit tongue-in-cheek here. Asynchronous I/O does make things a bit more complicated, but hopefully this example highlights the ultimate benefit of reactive code: we can handle more requests, and do more work, using asynchronous I/O on the same hardware if our work is I/O bound. If it’s CPU-bound (e.g.: fibonacci, bitcoin mining, or cryptography) then reactive programming won’t buy us anything.

Now, most of us don’t work with Channel or InputStream implementations for their day-to-day work! They think about things in terms of higher order abstractions. Things like the arrays, or, more likely, the java.util.Collection hierarchy. A java.util.Collection maps very nicely to an InputStream : they both assume that you’ll be able to work with all the data, near instantly. You expect to be able to finish reading from most InputStreams sooner rather than later. Collection types start to become a bit awkward when you move to larger sums of data; what happens when you’re dealing with something potentially infinite - unbounded - like websockets, or server-sent events? What happens when there’s latency between records? One record arrives now and another not for another minute or hour such as with a chat, or when the network suffers a failure?

We need a better way to describe these kinds of data. We’re describing something asynchronous - something that will eventually happen. This might seem a good fit for a Future<T> or a CompletableFuture<T> , but that only describes one eventual thing. Not a whole stream of potentially unlimited things. Java hasn’t really offered an appropriate metaphor by which to describe this kind of data. Both Iterator and Java 8 Stream types can be unbounded, but they are both pull-centric; you ask for the next record instead of having the type call your code back. One assumes that if they did support push-based processing, which lets you do more with your threads, that the APIs would also expose threading and scheduling control. Iterator implementations say nothing about threading and Java 8 streams all share the same fork-join pool.

If Iterator and Stream did support push-based processing, then we’d run into another problem that really only becomes an issue in the context of I/O: we’d need some way to push back! As a consumer of data being produced asynchronously, we have no idea when or how much data might be in the pipeline. We don’t know if one byte will be produced in the next callback or a if terabyte will be produced!

When you pull data off of an InputStream , you read as much data as you’re prepared to handle, and no more. In the examples above we read into a byte[] buffer of a fixed and known length. In an asynchronous world, we need some way to communicate to the producer how much data we’re prepared to handle.