Double-checked locking needs volatile on the instance field because object construction is not atomic and its steps can be reordered. Without volatile, another thread can see a non-null reference to a partially constructed object whose fields are still at their default values.
The pattern
class Singleton {
private static volatile Singleton instance; // volatile is mandatory
static Singleton get() {
Singleton local = instance; // 1st check (no lock)
if (local == null) {
synchronized (Singleton.class) {
local = instance;
if (local == null) { // 2nd check (locked)
local = new Singleton();
instance = local;
}
}
}
return local;
}
}
The point of the pattern is to avoid taking the lock on every read once the singleton exists — the first check is lock-free.
Why volatile is required
instance = new Singleton() is not one operation. It is roughly three:
Program-order intent: Legal reordering without volatile: 1. allocate memory 1. allocate memory 2. run constructor 3. publish reference <-- moved up! 3. instance = ref 2. run constructor <-- runs later
The JMM permits step 3 (publishing the reference) to be reordered before step 2 (running the constructor), because within the constructing thread there's no observable difference. But a second thread doing the first check (outside the lock) can now read a non-null instance that points at an object whose fields are still zero/null — and use it. That's a corrupted read of a "constructed" object.
Marking instance volatile forbids that reorder: the volatile write of the reference acts as a release, so the constructor's writes happen-before the publication, and the lock-free read acts as an acquire that sees them.