The fight for performance – Is reactive programming the right approach?
Reactive programming promises higher performance of Enterprise Java applications with lower memory requirements. This promise is achieved by avoiding blocking calls that always lead to process and context switches in the operating system. Such context switches have a high CPU and memory overhead, which, of course, is reduced by fewer of such switches. However, this performance gain of reactive programming comes at the price of poorer maintainability of the software. But is the higher performance worth the price and what are the alternatives? Let’s take a closer look at this in this article.
In the early days of Java, threading abstraction was a major advantage over other programming languages of that time. It still offers easy access to parallel programming and synchronization to developers today. Web frameworks could then be implemented very easily on this basis, since the binding of a web request to a thread in the servlet API made it possible to process a request virtually imperatively, without worrying about concurrency and synchronization. Before there was Tabbed Browsing and Ajax, one could also be quite sure via (web page) design that two requests of the same user session were never executed in parallel, which meant that the normal developer had practically no need to worry about parallel processing at the user session level.
The above-mentioned implementation of the Java threading abstraction currently has a serious disadvantage: Java threads are implemented as operating system processes so that every thread switch means a (very expensive) context switch in the operating system. At a time when web applications only had to process several thousand requests per minute, this was not a problem. In the meantime, however, the requirements for web applications have become significantly higher. Rising user numbers and more interactive SPAs (with more client-server communication) mean that today’s enterprise applications have to process noticeably more requests per minute than 15 years ago. The model in which a request is processed by an operating system thread reaches its limits. Especially when the request is blocked in between, e.g. when a database query is executed or when another microservice is called.
The degree of parallelism is significantly higher than at the time when the decision was made to implement Java threads as operating system processes. The requests and the code executed in them are now much shorter. This does not match the expensive context switches of the operating system processes.
This is where Reactive Programming comes in. The paradigm is exactly the opposite of the Java threading model. While the threading model tries to keep asynchronicity away from the user (“Everything happens in one thread”), in Reactive Programming asynchronicity is the principle. The program flow is seen as a sequence of events that can occur asynchronously. Each of these events is created by a Publisher. It does not matter on which thread the Publisher creates it. In a reactive application, the program code consists of functions that listen to this asynchronous publication of events, process them and, if necessary, publish new events.
This approach is particularly useful if you are working with external resources such as a database. In a classic Enterprise Java application, the system sends an SQL statement to the database and blocks until the database returns the result. With Reactive Programming, the statement is executed without waiting for the result. A method to submit a database query immediately returns a Publisher instead of blocking. The caller can register with this publisher to be informed when the database result is available. The result of the database is later published as an event on the Publisher.
This API is a useful alternative to the callback hell that otherwise can be found in asynchronous programming.
The advantage of Reactive Programming is that the code to be executed and the executing thread are decoupled. Thus, there are less expensive context switches on the operating system level.
Reactive programming, however, has a few serious drawbacks, as indicated in the introduction. The decoupling of written functions and executed code leads to an increased level of difficulty in reading and writing code. It is also complicated to write unit tests for such asynchronous code. Debugging the code is even more difficult.
With the integration into classic enterprise applications, further problems arise. There, the standard topics such as security, transactions or tracing are still attached to the current thread. When you start with Reactive Programming, this construct no longer works and other solutions have to be found.
Project Reactor, the foundation of Spring’s Reactive Web Framework, already has a bunch of auxiliary constructs that enable testing, debugging, and context propagation (see sections Testing, Debugging, and Context). However, the mere fact that such auxiliary constructs are required, already reveals the complexity of Reactive Programming. Therefore the question arises whether there are other feasible alternatives that solve the problem of expensive context switches of Java threads.
Alternative approaches to reactive programming
As mentioned above, the degree of parallelism of current Web applications in combination with the small size of the code snippets to be executed does not match the current threading implementation of Java, where each Java thread is assigned 1:1 to an operating system process. While there are ThreadPools in Java that allow you to run multiple of these code snippets without a context switch, that is just a clumsy workaround for the problem.
It is the same idea with all of these concepts. If I know that I am executing a long-running call (e.g. a database statement), my code should not block. I rather want to be able to specify the code to be executed when the long-running call is finished and the result is available. The way I would like to do this has to be easy to write and read. The resulting code should also be easy to debug and test.
Green threads in Java 1
Considering the effort required to make sure that the developer can write, test, maintain and debug his reactive program just as easily as he is used to forming the imperative world with standard threading abstraction, the question arises whether the better runtime performance justifies the use of Reactive Programming at all. If you then take a look at the alternative approaches just mentioned, it seems all the more questionable whether Reactive Programming is the right solution to the problem. However, one has to admit that the presented alternatives of other programming languages (i.e. Async Await and Coroutines) are language constructs and not third-party libraries. So would such alternative language constructs in Java make Reactive Programming superfluous? A look into the past of Java reveals an interesting aspect in this context:
In Java 1.1, the entire threading model was implemented as so-called “green threads”, i.e. the entire Java VM was running in just one operating system process. Java threads were implemented within the VM with their own scheduling algorithm. Thread switches and thus context switches within Java could be performed extremely quickly and with little memory overhead due to the virtual memory management of Java.
Another advantage of this solution was that the synchronization of data accesses within the Java applications was not so complicated. A “real” parallel access to a variable could not happen at all, because everything was executed in one operating system process and therefore was only “virtually parallel”.
However, the disadvantage of this implementation is that Java with Green Threads could only use one core or processor in multi-core or multi-processor systems. With Java programs, it was never possible to utilize the complete performance of the computers they were running on. In practice, it quickly became clear that this disadvantage was more serious than the mentioned advantages of this implementation.
Therefore, the use of Green Threads was quickly terminated. In Java 1.2 you could switch between Green and Native Threads via command line switch (see here). In Java 1.3 only native threads were supported. Now all cores and processors of a computer could be used. As a result, the effort for thread switches (which now were process switches) has increased significantly since then. So it happens that programming paradigms like Reactive Programming achieve much higher performance values, just because they do not block and therefore generate much fewer context switches.
Project loom combines the threading-models
Project Loom was launched last year (see here). The idea behind this JVM project is the redevelopment of Java support for green threads. Unlike in the past, these are not intended to replace the existing operating system threads, but to supplement them. Both threading models should, therefore, exist in parallel on the JVM and be used simultaneously in the program flow.
As a result, operating system threads continue to be implemented by the Java class Thread, and Green Threads by the newly invented Fiber class. If necessary, there will be a common base class. The plan is that most existing code can run without changes in fiber, without knowing the thread behind it. Thanks to Java’s virtual memory management, context switches between fibers should be possible with almost no overhead. Synchronization in Fibers should then also be much more efficient. One idea is that the scheduler ensures that two fibers that depend on each other (e.g. access the same variable) are executed on the same native thread. This ensures that they can never run in parallel. Synchronization is then practically no longer necessary.
To implement Fibers, the execution of threads in Java will be divided into two parts, the continuation, and the scheduler. A continuation represents an execution state, i.e. the code to be executed including the execution context such as call parameters, stack, etc. The scheduler then ensures that all continuations are evenly executed.
The separation of Scheduler and Continuation has several advantages. Until now, both scheduling and execution context was managed by the operating system. The separation now makes it possible to execute one of them (or both) in the JVM. Green Threads (the Fibers) can thus be implemented completely in Java and existing Java Schedulers such as the Fork-Join Pool can also be reused.
The separation of scheduler and continuation has another interesting aspect. The now separated continuations could be made available to every developer as a separate Java API. Continuation would be a language feature (e.g. under the name Coroutines) in Java. As written above, there are already several languages (also on the JVM) in which this language feature already exists. Project Loom would bring it to Java as a side-effect.
Reactive Programming solves performance problems caused by the use of native threads and the “One thread per request” paradigm. However, this solution goes along with higher development and maintenance complexity because testing and debugging, among other things, become more complicated.
Green threads are a possible way to avoid the performance losses caused by the process switches in the operating system. These were available in Java 1.1 but were already discarded in Java 1.3 because they did not allow the benefits of multi-core or multi-processor systems to be used. A new attempt to introduce another variant of Green Threads, so-called Fibers, into the JDK is Project Loom. This proposal would be accompanied by support for continuations in Java as a kind of spin-off product. This feature is known from other programming languages like Kotlin and Go under the name Coroutines.
You may be curious if and when Project Loom will be integrated into the JDK and what effects it will have on the distribution of Reactive Programming. From a performance point of view, this would probably make it superfluous for the Java world.
On that note, stay tuned.