Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Judging from the many interesting letters and suggestions I received, readers certainly connected with that article. Many readers were surprised, incredulous, and sometimes even angered to learn of some of the stranger aspects of the Java Memory Model (JMM). While the JMM is complicated and full of surprises, the good news is that if you follow the rules -- namely, synchronize whenever you read data that might have been written by a different thread or write data that will be read by a different thread -- you have nothing to fear from the JMM. But if you want to understand more about what's going on behind the scenes with concurrent programming, read on. Lots of readers tried to patch the holes in DCL; in this article I'll show why those holes are harder to patch than they might first appear.
DCL is a lazy initialization technique that attempts to eliminate the synchronization overhead on the most common code path. Here is an example of the DCL idiom:
Listing 1: The double-checked locking idiom (DCL)
class SomeClass {
private Resource resource = null;
public Resource getResource() {
if (resource == null) {
synchronized {
if (resource == null)
resource = new Resource();
}
}
return resource;
}
}
When first confronted with the possibility of unpredictable behavior associated with DCL, many programmers worry that Java might be broken somehow. One reader wrote:
Everything that I have heard about DCL gives me the creepy feeling that all sorts of strange and unpredictable things can happen when compiler optimizations interact with multithreading. Isn't Java supposed to protect us from accessing uninitialized objects and other unpredictable phenomena?
The good news is no, Java is not broken, but multithreading and memory coherency are more complicated subjects than they might appear. Fortunately, you don't have to become an expert on the JMM. You can ignore all of this complexity if you just use the tool that Java provides for exactly this purpose -- synchronization. If you synchronize every access to a variable that might have been written, or could be read by, another thread, you will have no memory coherency problems.
The Java architects strove to allow Java to perform well on cutting-edge hardware -- at the cost of a somewhat heavyweight and hard-to-understand synchronization model. This complicated model has led people to try and outsmart the system by concocting clever schemes to avoid synchronization, such as DCL. But the problems with DCL result from the failure to use synchronization, not with the Java Memory Model itself.
By far the most common category of suggested DCL fixes are those that try to fool the compiler into performing certain operations in a specific order. Attempting to trick the compiler proves dangerous for many reasons, the most obvious being that you might succeed in only fooling yourself into thinking that you've fooled the compiler. Believing that you've tricked the compiler can give you a false sense of confidence, when maybe you've only fooled this version of this compiler in this case.
The Java Language Specification (JLS) gives Java compilers and JVMs a good deal of latitude to reorder or optimize away operations. Java compilers are only required to maintain within-thread as-if-serial semantics, which means that the executing thread must not be able to detect any of these optimizations or reorderings. However, the JLS makes it clear that in the absence of synchronization, other threads might perceive memory updates in an order that "may be surprising."
One commonly suggested fix for DCL is to use a temporary variable to try and force the constructor to execute before its reference is assigned:
Listing 2: Using a temporary variable
public Resource getResource() {
if (resource == null) {
synchronized {
if (resource == null) {
Resource temp = new Resource();
resource = temp;
}
}
return resource;
}
In Listing 2, you might think that as-if-serial semantics requires that the construction complete before resource is set, but that view is only from the perspective of the executing thread. Actually, the compiler is free to completely
optimize away the temporary variable. Though numerous tricks have been suggested to prevent the compiler from optimizing away
the temporary variable, such as making it public, the compiler can still vary the order in which assignments are made inside
the synchronized block as long as the executing thread can't tell the difference.
Another common suggestion is to use a "guard" variable to indicate that the initialization has completed. An example of this technique:
Listing 3: Using a guard variable
private volatile boolean initialized = false;
public Resource getResource() {
if (resource == null || !initialized) {
synchronized {
if (resource == null)
resource = new Resource();
}
initialized = (resource != null);
}
return resource;
}
At first, the approach taken in Listing 3 looks promising. Because initialized is set after the synchronized block exits, it appears that resource will have been fully written to memory before initialized is set. Listing 3 even attempts to ensure that initialized is not set until resource is set by making initialized's value depend on resource's value. However, synchronization doesn't work quite so literally. The compiler or JVM can move statements into synchronized blocks to reduce the cache-flush penalties associated with synchronization. But this means that from the perspective
of other threads, initialized could still appear to be set before resource, and all of resource's fields are flushed to main memory.
It's hard to fool the compiler, even if you can think like one. But if you do succeed in tricking the compiler, you still aren't guaranteed correct programs with respect to the JMM. Compiler optimizations are only one source of potential reorderings; the processor and the cache can also affect the order in which other threads perceive memory updates. A modern processor can execute multiple instructions simultaneously, or out of order, as long as it can determine that the results of one operation are not required for another operation. Also, write-back caches might vary the order in which memory writes are committed to main memory.
synchronized