Skip to main content

JSR 133 in Public Review

April 13, 2004

{cs.r.title}








Contents
If at first you don't succeed
What's a memory model?
Memory models are an abstraction for optimizations
Sorry, this variable is out of order
Synchronization and reordering
Informal memory model semantics
Initialization safety
For further reading

JSR 133, which was charged with fixing the problems
discovered in the Java Memory Model (JMM), has recently entered public review after nearly three years in committee. The new memory model strengthens the semantics of volatile and final, largely to bring the language semantics into consistency with common intuition.

JSR 133 is probably one of the most important JSRs to come along in quite a while, despite the fact that it produced no code, no APIs, and no new language features. What it did produce is a formal mathematical specification for the semantics of synchronized, volatile, and final--a spec that most developers will never read, nor ever want to, because of its complexity. So why is it so important? Quite simply, it provides the foundation for (finally) delivering on Java's promise of being able to develop write-once, run-anywhere concurrent applications.

If at first you don't succeed

The original Java Language Specification included a formal memory model for the semantics of multithreaded Java programs, which was a significant step forward in enabling developers to write portable multithreaded applications. While the initial memory model was ambitious and pioneering, it turned out to not mean exactly what the architects intended, nor was it consistent with the intuition of many developers, even those versed in writing multithreaded code. On top of that, it was difficult to understand. (Broken and hard to understand is a bad combination.)

In 1999, Bill Pugh, a professor at the University of Maryland, published a paper entitled The Java Memory Model is Fatally Flawed. In it, he observed that the memory model, as specified, prohibits many desirable compiler optimizations, and that, surprisingly, unsynchronized access to immutable objects (such as Strings) is not thread-safe, and that final fields can even appear to change their values. Needless to say, this was not what was intended, and in 2001, the JSR 133 Expert Group was formed to fix the problems discovered with the Java Memory Model. The goals of JSR 133 included:

  • Preserve existing safety guarantees, like type-safety, and
    strengthen others. Variable values may not be created "out of thin
    air" -- each value for a variable observed by some thread must be a
    value that can be reasonably placed there by some thread.

  • The semantics of correctly synchronized programs should be as simple
    and intuitive as possible.

  • Developers should be able to reason confidently about how
    multithreaded programs interact with memory.

  • It should be possible to develop correct, high-performance JVM
    implementations across a wide range of popular hardware
    architectures.

  • Provide a new guarantee of initialization safety. If an object
    reference is properly published (which means that references to it do
    not escape during construction), then all threads that see a
    reference to that object will also see the values for its final fields
    that were set in the constructor, without the need for
    synchronization.

  • There should be minimal impact on existing code.

The resulting formal specification is not for the timid -- significant mathematical sophistication and understanding of processor architecture and compiler optimization is needed to fully understand it. However, the JSR 133 group has also produced formal proofs that the new memory model has various desired properties -- it permits many common optimizations (reordering, unrolling and merging, speculative reads) and that correctly synchronized programs exhibit sequentially consistent behavior -- which means, basically, that we can take their word that it is correct. Fortunately, the group also produced a set of informal semantics that are far easier to understand.

What's a memory model?

So what is a memory model, anyway? A memory model describes the relationship between variables in a program (instance fields, static fields, and array elements) and the low-level details of storing them to and retrieving them from memory in a real computer system. Theoretically, all variables are stored in memory, but the most current value for a given variable may not always be visible to all other threads. This could happen because of actions taken by the compiler (for example, optimizing a loop index variable by storing it in a register), the runtime, or the hardware (the cache may delay flushing a new value of a variable to main memory until a more opportune time).

Memory models are an abstraction for optimizations

The Java Memory Model is an abstraction meant to describe a range of
allowable optimizations by processors, caches, and compilers. In the
JMM, all variables are stored in memory, of which there are two kinds
-- main memory (shared by all threads), and local memory (specific to
a given thread). When a thread modifies a variable, the change is
made in local memory, and should eventually be reflected in main
memory, but in the absence of synchronization, the time between the
update to local memory and the corresponding update to main memory may
be indefinite. If a variable is updated by one thread in main memory,
in the absence of synchronization, that update may not be immediately
visible to all other threads if there is already a value for that
variable in the other thread's local memory.

This abstract model is sufficient to describe a wide range of common
optimizations, such as hoisting variables into processor registers,
delays in flushing or invalidating per-processor memory caches in
weak-memory-model multiprocessor systems, out-of-order execution by
processors, and reordering of instructions by compilers. All of these
optimizations are in aid of better performance, and most of the time,
they don't cause any trouble for the programmer. For example,
processor-local caches both speed access to needed data and reduce
traffic on the shared memory bus, but entail risks of stale data,
inconsistent views of memory, and lost updates. When multiple threads
need to access the same variable, the memory model steps in to define
the conditions under which one thread is guaranteed to see the most
recent value for a variable written by another thread.

Sorry, this variable is out of order

The Java Memory Model concerns itself with when it is allowable to
reorder reads or writes to variables with respect to each other. Just
as in special relativity, ordering is relative to the observer --
operations that occur in one order in the executing thread can appear
to execute in a different order to another thread. For example,
thread A may write to two different variables in a given order, but
because of the unpredictable timing of cache flushing, another
processor could see those writes appear to have executed in the
opposite order.

In general, compilers, processors, and caches can take significant
liberties with the timing and ordering of memory reads and writes,
unless synchronization is used to induce an ordering on certain memory
operations. Without proper synchronization, some surprising things
can happen. Whenever you are writing a variable that might next be
read by another thread, or reading a variable that might have last
been written by another thread, you must synchronize. The listing
below shows an example where reordering is possible. Say thread A
executes the writer() method and thread B executes the
reader() method, and thread B sees the value 2 in
r1. You might be tempted to assume that r2
would therefore have the value 1, since the write of x
occurs before the write of y. But this would be a bad
assumption -- the writes could have been reordered by the compiler,
the processor, or the cache.

class Reordering {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}

Synchronization and reordering

Many developers mistakenly assume that synchronization is simply a
shorthand for "critical section" or "mutex". While mutual exclusion
is one element of the semantics of synchronization, there are two
additional elements -- visibility and ordering. When thread A exits a
synchronized block protected by monitor M, and thread B later enters a
synchronized block protected by monitor M, the Java Memory Model
guarantees that any memory write which was visible to A at the time it
exited the block (released the monitor) will be visible to B at the
time it enters the block (acquires the monitor.) What do we mean by
visible? It means that from the perspective of thread B, the write(s)
in question which were performed by A before releasing the monitor
will not be reordered with reads by B that follow acquiring the
monitor. The Java Memory Model places restrictions on the scope of
reorderings in the presence of synchronization, and this is how we
guarantee that threads can have a consistent view of variables shared
across more than one thread.

A JVM implementation on a multiprocessor system will generally
invalidate its cache when entering a synchronized block (so as to
guarantee that subsequent reads will come from main memory) and will
flush its cache when leaving a synchronized block (so as to make
writes performed during or before the synchronized block visible to
other processors.) But that doesn't mean that uniprocessor systems
are immune from reordering problems -- reorderings can come from other
sources besides caches, such as compiler optimizations.

Informal memory model semantics

The new memory model semantics create a partial ordering, called
happens-before, on memory operations (read, write, lock, unlock) and
other thread operations (Thread.start() and
Thread.join()). Since it is an ordering, the
happens-before relation is transitive and antisymmetric. When one
action happens before another, the first is guaranteed to be ordered
before, and therefore visible, to the second. The rules for
happens-before are as follows:

  • Program order rule. Each action in a thread happens before every
    action in that thread that comes later in the program's order.

  • Monitor rule. An unlock on a monitor happens before every
    subsequent lock on the same monitor.

  • Volatile rule. A write to a volatile field happens before every
    subsequent read of the same volatile.

  • Thread start rule. A call to start() on a thread happens before
    any actions in the started thread.

  • Thread join rule. All actions in a thread happen before any other
    thread successfully returns from a join() on that thread.

The third of these rules, the one governing volatile fields, is a
stronger guarantee than that made by the original memory model. This
is useful because now volatile variables can be used as "guard"
variables -- you can now use a volatile field to indicate across
threads that some set of actions has been performed, and be confident
that those actions will be visible to all other threads.

To prove that a write made in a synchronized block in one thread is
visible to another thread executing a synchronized block protected by
the same monitor, we can apply the rules above to the code below.

class Reordering {
  int x = 0, y = 0;
  Object l = new Object();

  public void writer() {
    x = 1;
    synchronized (l) {
      y = 2;
    }
  }

  public void reader() {
    synchronized (l) {
      int r1 = y;
      int r2 = x;
    }
  }
}

A thread calling reader() will now see the values of
x and y placed there by the thread calling
writer(). The writer() method contains four
actions -- write to x, lock l, write to
y, and unlock l. By the program order rule,
the writes to x and y happen before the
unlock of l. Similarly, the reader() method
contains four actions -- lock l, read x,
read y, and unlock l, and again by the
program order rule the reads of x and y
happen after the lock operation. Since by the monitor rule, the
unlock operation in writer() happens before the lock
operation in reader(), we can see (by transitivity) that
the writes to x and y in
writer() happen before the reads of x and
y in reader(), and therefore the correct values
are visible to the thread calling reader().

Initialization safety

The new memory model semantics also include a new guarantee of
initialization safety for objects with final fields. As long
as the object is constructed correctly, which means that a reference
to the object is not published to other threads before the constructor
completes, the values assigned to the final fields in the constructor
will be visible to all other threads without synchronization. In
addition, the visible values for any other object or array referenced
(directly or indirectly, such as fields of objects referenced by a
final field or elements of a final array) by those final fields will
be at least as up to date as the final fields.

If this guarantee doesn't sound new to you, that's not surprising --
it has long been assumed (as was the intention of the Java architects)
that immutable objects (whose immutability can be guaranteed by final
fields) are inherently thread-safe. And now that the memory model has
been fixed, this intuition is now (finally) correct.

The changes to the memory model, which include strengthening the
semantics of volatile and final, are officially part of JDK 1.5.
However, Sun JDKs as early as 1.4 conform (unofficially) to the
semantics laid out by JSR 133.

For further reading

Brian Goetz has been a professional software developer for the past 17 years.
Related Topics >> JSR   |