Java’s concurrency support has changed significantly over the last 20 years to mostly reflect the changes in hardware, software systems, and programming concepts.
Going through the evolution of Java concurrency support can help in understanding the reason for the new additions and their roles.
Java’s Initial Concurrency Support
Initially, Java had threads and built-in monitor objects, which are supported by locks (via synchronized classes and methods), Runnable and Thread interfaces.
The built-in monitor objects allow mutual exclusion and coordination among threads.
The focus was on basic multi-threading and synchronisation primitives.
They are efficient but low-level and very limited in capabilities. They are also difficult and error-prone to use, which can cause accidental complexity
Java 5 Concurrency Changes
In 2004, Java 5 introduced new concurrency support: Java Executor framework, synchronizers, blocking queues, atomics, and concurrent collections.
It was a big step forward with better support compared to the earlier one.
The idea of thread pools as a higher-level idea capturing the power of threads allowed developers to decouple task submission from task execution.
It also enabled coarse-grained task parallelism (there was not a lot of computing power at that time), which is
- Submit tasks to execute so that they can run in a pool of worker threads
- The results are computed and put into a queue
- The queue can be accessed by other threads to get the results.
Also by Future and Callable interfaces, it provided higher-level and result-returning variants of Runnable and Thread.
It is feature-rich and optimized but also difficult and error-prone as still uses low-levels of abstraction.
Java 7 Concurrency Changes
Java 1.7 added foundational parallelism support.
It provided data parallelism that runs the same tasks on different data elements. It added RecursiveTask to support fork-join approach of divide-and-conquer algorithms.
Fork-join approach is powerful and scalable but tricky to program correctly
Java 8 Concurrency Changes
Java 1.8 provided advanced level parallelism, which is support for streams and their parallel processing by building on the newly added support for lambdas.
So, the focus was on functional programming for data parallelism and asynchronous programming. It added support for composing Futures via the CompletableFutures( implementation of Future)
It provided an effective balance between productivity and performance
Java 9 Concurrency Changes
Changes Java 9, provided explicit support for distributed asynchronous programming.
It is basically the Flow API based on the publish-subscribe protocol, including back-pressure, and forms the basis for reactive programming. It is a set of interfaces for publish-subscribe model to standardize the concepts to maximize the interoperability of different implementations.
Which one to use?
As the new changes came in, the level of abstraction has been increased.
It is good to learn the constructs on all levels.
Learning low-level constructs will help in understanding the high-level constructs better and how they work underneath.
As a rule of thumb, it is always good to use the simplest and most efficient option. So, it is better to start with using the most high-level option, which is Java 8’s concurrency/parallelism frameworks (i.e parallel streams and completable futures) or Java 9’s reactive programming) if it fits your needs. Consider using lower-level constructs only if needed, which should be rare at the application level.