Check this answer I gave here on StackOverflow a long time ago for an explanation on why that DCL fails, and how to fix it.
The problem is not sync/async. The problem is something called reordering.
The JVM spec defines something called a happens-before relationship. Inside a single thread, if statement S1 appears before statement S2, then S1 happens-before S2, that is, whatever modifications S1 made to the memory are visible to S2. Note that it does not say that the statement S1 must be executed before S2. It just says that things should look as if S1 was executed before S2. For example, consider this code:
int x = 0;
int y = 0;
int z = 0;
x++;
y++;
z++;
z += x + y;
System.out.println(z);
Here, it doesn't matter the order in which the JVM executes the three increment statements. The only guarantee is that, when running z += x + y
, the values of x, y, and z, must be all 1. In fact, the JVM is actually allowed to reorder statements if the reordering does not violate the happens-before relationship. The reason for this is that sometimes a little reordering can optimize your code, and you get better performance.
The drawback is that the JVM is allowed to reorder things in a way that could lead to very strange results when you use multiple threads. For example:
class Broken {
private int value;
private boolean initialized = false;
public void init() {
value = 5;
initialized = true;
}
public boolean isInitialized() { return initialized; }
public int getValue() { return value; }
}
Suppose a thread is executing this code:
while (!broken.isInitialized()) {
Thread.sleep(1); // patiently wait...
}
System.out.println(broken.getValue());
Suppose that, now, another thread does, on the same Broken
instance,
broken.init();
The JVM is allowed to reorder the code inside the init()
method, by first running initialized = true
, and only then setting the value
to 5. If this happens, the first thread, the one waiting for the initialization, might print 0! To fix, either add synchronized
to both methods, or add volatile
to the initialized
field.
Back to the DCL, it is possible that the initialization of the singleton gets executed in a different order. For instance:
1. mem = allocateMem() : allocate memory for Test and save it's address.
2. construct(mem) : construct the class Test
3. t = mem : point t to the mem
could become:
1. mem = allocateMem() : allocate memory for Test and save it's address.
2. t = mem : point t to the mem
3. construct(mem) : construct the class Test
because, for a single thread, both blocks are completely equivalent. That said, you can be safe that this kind of singleton initialization is completely safe for a single-threaded application. However, for multiple threads, one thread might get a reference for a partially initialized object!
To ensure a happens-before relationship between statements when you use multiple threads, you have 2 possibilities: acquiring/releasing locks and reading/writing volatile fields. To fix the DCL, you must declare the field that holds the singleton volatile
. This will make sure that the initialization of the singleton (ie, running its constructor) happens-before any read of the field holding the singleton. For a somewhat detailed explanation on how volatile fixes the DCL, check the answer I linked on the top of this one.