Consider the following listing:
long foo;
...
void doIt() {
if (foo==0) {
synchronized (this) {
if (foo==0) {
foo = System.currentTimeMillis();
} // if
} // synchronized
} // if
ready()
}
The code looks right, as far as it goes. However, it is possible for a thread to call the ready method before the assignment to foo is complete. The root of the problem is the way that Java handles assignments to long and double variables. Unless they are declared volatile, implementations of Java are allowed to treat assignments to long and double variables as two assignments to two variables that hold 32 bits each rather than one assignment to single variable that holds 64 bits. In other words, implementations of Java are allowed to implement assignments to long or double variables as two separate operations.
If the code in question is running in an environment that implements assignments to long as two operations, it is possible for a thread to test for foo equal to zero when only half of foo has been assigned. This can produce incorrect results.
You can force the code to have the intended behavior by declaring foo to be volatile, like this:
volatile long foo;
However, a compiler is likely to implement the semantics of volatile by implicitly generating a synchronized statement around each place that the variable is accessed. This defeats the purpose of the Double-Checked Locking pattern, which is to avoid unnecessary synchronization delays. I do not recommend using the Double-Checked Locking pattern for the initialization of long or double variables, unless you know that your program will be running only in environments that treat assignments to long or double as a single operation.
There is another subtlety of Java that can cause a similar problem. The Java language specification allows an implementation of Java to execute operations in a somewhat different order than specified by source code. The actual rules are a bit complicated; however they guarantee that the thread the executes the operation will execute as if the operations were being executed in the specified order. In particular, the rules make if possible for a value to be assigned to a variable as soon as it becomes inevitable that the assignment will be made, unless the variable is declared volatile. For example, consider the statement
x = new Bar();
The language specification allows the assignment to x to be done before the Bar object’s constructor has returned. The reason that the language specification allows this sort of reordering of operations is to allow compilers to perform some optimizations.
Theoretically, this is a very serious problem that can break many applications of the Double-Checked Locking pattern. In practice, it is not a serious problem at all.
The reason it is not a serious problem is that compiler writers know
this sort of optimization can break programs. If the default setting for
a compiler caused it to perform optimizations that break programs, not
many people would want to use the compiler. As a practical matter, a compiler
will not perform unsafe operation on programs unless you explicitly set
options for it to do so. The real implication of this is that if you use
the Double-Checked Locking pattern, be careful about what optimization
you enable.