JSR 133 in Public Review Blog

来源:互联网 发布:p2p类似软件 编辑:程序博客网 时间:2024/05/16 07:46
          

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 ofsynchronizedvolatile, andfinal--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 ofy. 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, calledhappens-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 tostart() 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 ajoin() 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, lockl, write to y, and unlock l. By the program order rule, the writes to x andy 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 inwriter()happens before the lock operation inreader(), we can see (by transitivity) that the writes to x and y in writer()happen before the reads of x and y inreader(), and therefore the correct values are visible to the thread calling reader().

Initialization safety

The new memory model semantics also include a new guarantee ofinitialization 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

  • The Java memory model page.
  • The Java Memory Model FAQ.
  • Part 1 and Part 2 of this author's series for IBM developerWorks "Fixing the Java Memory Model".



0 0
原创粉丝点击