Chapter 14. Concurrency on the JVM

Mario Fusco

Originally, raw threads were the only concurrency model available on the JVM, and they’re still the default choice for writing parallel and concurrent programs in Java. When Java was designed 25 years ago, however, the hardware was dramatically different. The demand for running parallel applications was lower, and the concurrency advantages were limited by the lack of multicore processors—tasks could be decoupled, but not executed simultaneously.

Nowadays, the availability and expectation of parallelization has made the limitations of explicit multithreading clear. Threads and locks are too low-level: using them correctly is hard; understanding the Java Memory Model even harder. Threads that communicate through shared mutable state are unfit for massive parallelism, leading to nondeterministic surprises when access isn’t properly synchronized. Moreover, even if your locks are arranged correctly, the purpose of a lock is to restrict threads running in parallel, thus reducing the degree of parallelism of your application.

Because Java does not support distributed memory, it’s impossible to scale multithreaded programs horizontally across multiple machines. And if writing multithreaded programs is difficult, testing them thoroughly is nearly impossible—they frequently become a maintenance nightmare.

The simplest way to overcome the shared memory limitations is to coordinate threads via distributed queues instead of locks. Here, message passing replaces shared memory, which also improves decoupling. Queues are good for unidirectional communication but may introduce latency.

Akka makes the actor model, popularized by Erlang, available on the JVM, and is more familiar to Scala programmers. Each actor is an object responsible for manipulating only its own state. Concurrency is implemented with message flow between actors, so they can be seen as a more structured way of using queues. Actors can be organized in hierarchies, providing for built-in fault tolerance and recovery through supervision. Actors also have some drawbacks: untyped messages don’t play well with Java’s current lack of pattern matching, message immutability is necessary but cannot currently be enforced in Java, composition can be awkward, and deadlocking between actors is still possible.

Clojure takes a different approach with its built-in software transactional memory, turning the JVM heap into a transactional data set. Like a regular database, data is modified with (optimistic) transactional semantics. A transaction is automatically retried when it runs into some conflict. This has the advantage of being nonblocking, eliminating many problems associated with explicit synchronization. This makes them easy to compose. Additionally, many developers are familiar with transactions. Unfortunately, this approach is inefficient in massively parallel systems where concurrent writes are more likely. In these situations retries are increasingly costly and performance can become unpredictable.

Java 8 lambdas promote the use of functional programming properties in code, such as immutability and referential transparency. While the actor model reduces the consequences of mutable state by preventing sharing, functional programming makes the state shareable because it prohibits mutability. Parallelizing code made of pure, side-effect-free functions can be trivial, but a functional program can be less time efficient than its imperative equivalent and may place a bigger burden on the garbage collector. Lambdas also facilitate the use of the reactive programming paradigm in Java consisting in asynchronous processing of streams of events.

There is no silver bullet for concurrency, but there are many different options with different trade-offs. Your duty as a programmer is to know them and choose the one that best fits the problem at hand.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.247.196