The reason the
volatile keyword is necessary may indeed be obscure, but the memory issue fix in Java 5 does NOT make its use in that example code optional.
Java 5 introduced a new memory model that, among other things, ensures that the way
volatile is used in that example code will indeed work. Before Java 5, all bets were off.
So why is
volatile still necessary for the double-checked locking
pattern, even today?
The JVM is free to reorder instructions as it sees fit, as long as it doesn't affect the final result
as observed by the thread executing those instructions. Importantly, that guarantee does not take other threads into account!
Let's say thread 1 has entered the synchronized block and has just finished assigning a reference to
uniqueInstance. Thread 2 is currently executing the first
if-statement. Thread 2 sees that
uniqueInstance is assigned, and so returns the reference immediately.
The calling code that thread 2 returns to might call methods on the returned singleton object. Without
volatile, those calls might crash or lead to unexpected results because the singleton object has not been properly initialized.
How is that possible? Thread 1 initialized the singleton object and then assigned it to the
uniqueInstance field, right? Well no, not necessarily.
You see, thread 1 might have assigned a memory address to the field BEFORE it went on to initialize the new object. It's free to reorder instructions, after all. To thread 1, it doesn't matter whether it assigns the field first, or whether it initializes the object first.
The
volatile keyword really only does one thing: whenever you access a volatile field, all instructions that
happened-before accesses to that same field in a different thread, unscramble!
The term
happens-before has a very exact and technical definition. For the messy details,
you should refer to the JLS and the JVMS. In this particular example, it means the following:
From the viewpoint of thread 1, initialization of the object
happens-before the assignment of the static field, ALWAYS. However, this is just a point of view, it may not reflect reality.
Without
volatile, thread 2 may not agree. When you make the field
volatile however, thread 2 will now agree with thread 1 that the initialization of the object
happens-before the assignment of the field.