This is Part Four of a series of articles on Java.next. In Part Four, I will begin to explore how the Java.next languages (JRuby, Groovy, Clojure, and Scala) deal with concurrency. Concurrency is a big topic, so I will subdivide it, narrowing my focus in this part to how the Java.next languages support immutability.
Over the last decade, most programmers have written code with little concern for concurrency. Often this has caused problems later, when programs needed to be used in a more concurrent setting. This is only going to get worse: in the future, everything is concurrent.
Why don't programmers do a better job with concurrency? Because it is hard. Read the excellent Java Concurrency in Practice (JCIP), and you will come away impressed with all the clever work that has gone into making Java a good environment for writing concurrent applications. But may also come away thinking "Who is smart enough to get this kind of code right in production?"
Not many people can, and the experts agree that we need an entirely different paradigm for writing concurrent applications. It may even be the case that supporting concurrency is the number one priority for Java.next.
One of the most difficult things about writing concurrent programs is deciding how to protect mutable shared state. Java provides a locking primitive, synchronized
, but locking primitives are difficult to use. You have to worry about
To make matters worse, it is generally impractical to write automated tests that explore the various possible interleavings of events.
Immutability solves all these problems of locking, by removing the need for ever locking at all. Immutable state can always be shared safely. Nobody can ever see a corrupt value by definition, as values never change.
All of the Java.next languages support immutability to some degree, as I will show below.
JCIP uses a ThreeStooges
class as an extremely simple example for creating immutable objects in Java. Continuing the theme of that example, here is JRuby's immutable stooges:
In Ruby, you can set immutability on a per-instance basis, as opposed to a per-class basis. A particular object can become immutable by calling freeze
. If the internal components of that object are not primitive, they will need to freeze
as well. So, in the constructor above, I freeze
the two things at risk of changing: stooges
and self
.
While immutability is possible in Ruby, be warned. Heavy use of freeze
to enable safe concurrency is far from idiomatic.
In Groovy, the ThreeStooges
class looks and works like Java, but prettier:
The important details here are that the internal stooges
collection is
final
(cannot be changed after the object is created)private
(cannot be accessed outside the object itself)I could also wrap stooges
with unmodifiableSet
, but since the ThreeStooges
class already wraps stooges
without exposing a modifying method, the second wrap is redundant.
In JRuby and Groovy, immutability is possible. In Scala, immutability is preferred. The default collection classes are immutable, so ThreeStooges
is just
A few observations here:
val
(immutable) or var
(changeable). Scala style encourages you to use val
where possible.Set
is immutable.I find that Scala's inline constructor syntax and braces-free method definition syntax make the Scala definition easier to read than the JRuby or Groovy versions.
In Clojure, immutability is the default. In fact, everything is immutable, with only two exceptions:
So, the three-stooges
can just be a set:
A few observations:
[& names]
is Clojure's varargs syntax.(stooges name)
can be read as "find name
in the set stooges
"To see how immutables can simplify an implementation, consider the Factorizer
servlet example from JCIP. The objectives of this example are to
Here is a Java version of the factorizer using synchronized blocks, and stripped down to bare essentials for clarity:
In order for the CachedFactorizer
to be correct and efficient, you need to think carefully about where the synchronized blocks should go. In the example above, there are two synchronized blocks. The first block checks the cache, and updates the counters. It protects:
hits
cacheHits
lastFactors
The second block updates the cache. It protects:
lastNumber
lastFactors
To make sure you understand the approach, ask yourself the following questions:
That's a lot to think about. Now, let's consider the same example, but using immutable data structures.
Here is one Clojure approach to cached-factor
:
There are several things to notice here:
hits
, cache
, and cache-hits
take advantage of Java 5's atomic wrapper classes. (The #^ClassName
syntax adds type information.)incrementAndGet
method is used to update the two hit counters, and the cache
is pulled into a local variable to avoid inconsistent reads.The real key to this approach, however, is storing a Clojure map in an AtomicReference
. Because Clojure data structures are immutable, they can benefit from an AtomicReference
in a way that mutable classes cannot.
The immutable approach looks only a little simpler in this small example. But the benefit of using immutable data increases when you compose objects together. If you wanted to compose the synchronized version with another object, you would have have to dig back into the internals of both objects, study their locks, and pick a new lock strategy to cover the two objects together. Composing operations with immutable objects is much easier.
Since Scala's default collections are immutable, a Scala approach can closely parallel the Clojure code above:
While this is similar to the Clojure approach, the difference is instructive. While the Clojure version stores the cache as a simple Map, the Scala version introduces a strongly-typed Cache
class for the cache of values and their factors. The differences here are intended to idiomatic. Either approach could work in either language.
Could you write the example above in Groovy, JRuby, or even Java? Yes, but it would be non-idiomatic, even ugly. I am not going to show the JRuby and Groovy versions here, because those languages do not offer any concurrency-specific advantages over Java. Scala and Clojure, on the other hand, don't just make immutable objects possible. They make them easy and idiomatic.
Languages are designed to support certain priorities, inevitably at the expense of others. By making immutability a preferred option (Scala) or the standard (Clojure), these languages are encouraging a different paradigm for concurrent applications.
Languages are not about what they make possible, but about what they make beautiful. Clojure and Scala aim to make concurrent programs beautiful, and their preference for immutability is but the tip of the iceberg. In the next installment of this series, I will explore how Clojure and Scala enable concurrent programs through actors, agents, and software transactional memory.