Lazy Singletons in Java: Why Double-Checked Locking Needs "volatile", and the Better Alternative That Doesn't
A practical guide to lazy initialization, instruction reordering, and the pattern that sidesteps the whole problem.
If you've ever read about the singleton pattern in Java, you've almost certainly run into the phrase "double-checked locking," usually followed immediately by a stern warning: don't forget the volatile keyword, or it's broken. That warning is correct — and it leads people to a tempting but wrong question: can I just do double-checked locking without volatile?
The honest answer is no. Double-checked locking's correctness depends on volatile; remove it and the pattern is broken, full stop. But that's not the end of the story, because it points to a much better question:
If I want a lazy, thread-safe singleton, is there a way that doesn't make me reason about volatile and memory barriers at all?
There is — and the trick is not to be clever with locks, but to hand the hard part to the JVM, which already solved it for you. This post walks through why the naive approaches fail, what volatile is actually protecting against in double-checked locking, and how a different pattern — the initialization-on-demand holder idiom — sidesteps the whole problem without a single volatile or synchronized of your own.
A note up front, so the framing is clear: the holder idiom is not double-checked locking with the volatile removed. It's a different pattern that replaces double-checked locking. We'll build up to it by first understanding exactly why DCL can't drop volatile, which is what makes the alternative so satisfying.
A quick refresher: what is a lazy singleton?
A singleton is a class that should only ever have one instance. A lazy singleton is one where that single instance is created the first time someone asks for it, rather than when the class is first loaded. The opposite is eager initialization, where the instance is built up front whether you end up using it or not.
Laziness is appealing when constructing the object is expensive — opening a database connection pool, loading a big config file, spinning up a thread pool — and you'd rather not pay that cost unless the object is actually needed.
Here's the textbook lazy version:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
This works perfectly in a single-threaded program. The trouble starts the moment two threads call getInstance() at the same time.
Why the naive version breaks under concurrency
Imagine two threads, A and B, both calling getInstance() for the very first time, nearly simultaneously.
Thread A checks instance == null, sees true, and starts building the object. Before it finishes, Thread B also checks instance == null, also sees true (because A hasn't assigned anything yet), and also starts building. Now you have two instances — the one thing a singleton is supposed to prevent.
The obvious fix is to put a lock around the whole thing:
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
This is correct. The synchronized keyword means only one thread can be inside the method at a time, so the race disappears. But it has a cost: every single call to getInstance() has to acquire and release the lock — forever. Even years after the instance was created and the if check is guaranteed to be false, every caller still queues up for the lock. For a method that might be called millions of times, that's a lot of overhead protecting against a race that can only happen once.
Enter double-checked locking
Double-checked locking (DCL) is the classic attempt to get the best of both worlds: lazy, thread-safe, and lock-free after the first call. The idea is to check instance == null twice — once without the lock (the fast path) and once with it (the safe path):
public class LazySingleton {
private static LazySingleton instance; // no volatile yet
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) { // first check, no lock
synchronized (LazySingleton.class) {
if (instance == null) { // second check, with lock
instance = new LazySingleton();
}
}
}
return instance;
}
}
The logic reads beautifully. Once instance is set, the outer if is false and we return immediately without ever touching the lock. We only synchronize during the brief window when the object is being created. It looks airtight.
It is not. This exact code is the famous broken double-checked locking, and the reason it's broken is subtle enough that it fooled the Java community for years.
The real culprit: instruction reordering
The problem hides inside this one innocent line:
instance = new LazySingleton();
This looks like a single, indivisible operation. It isn't. Underneath, the JVM turns it into roughly three separate steps:
Allocate a chunk of memory for the new object.
Run the constructor to initialize the object's fields.
Assign the address of that memory to the
instancevariable.
In the order you'd expect — allocate, construct, assign — everything is fine. By the time instance becomes non-null (step 3), the object is fully built (step 2 already ran). Any other thread that sees a non-null instance is guaranteed to see a complete object.
But here's the catch the Java Memory Model allows: steps 2 and 3 can be reordered. From the perspective of the thread doing the work, swapping "construct" and "assign" produces an identical result — it's a single thread, it always sees its own writes in a consistent way, so the JVM and CPU are free to reorder them for performance.
So the actual execution might be: allocate, assign, construct. And now there is a dangerous window. After the assign but before the constructor finishes, instance is non-null but the object behind it is still half-built — its fields are still at their default values (null, 0, false).
This is fine for the constructing thread. But picture a second thread arriving during that window. It hits the very first line, if (instance == null), sees that instance is non-null, skips the lock entirely, and returns the reference. It just handed back a singleton whose fields haven't been initialized yet. The bug is rare, timing-dependent, and varies by hardware — which makes it one of the nastiest kinds to track down.
An analogy
Think of a hotel room with a "Ready / Not ready" sign on the door. The correct procedure is: clean the room, then flip the sign to "Ready." Reordering is the housekeeper flipping the sign to "Ready" before making the bed. To the housekeeper, no problem — they know they'll finish in a moment. But a guest walking past reads "Ready," walks in, and finds an unmade bed. The guest is the second thread; the prematurely flipped sign is the reference being published before the constructor runs.
What volatile does — and why double-checked locking can't drop it
The standard fix is to declare the field volatile:
private static volatile LazySingleton instance;
volatile gives two guarantees that together close the hole. First, it forbids the specific reordering — the write to instance cannot be moved ahead of the constructor, so the reference is only published once the object is genuinely finished. Second, it establishes a happens-before relationship: when another thread reads the non-null instance, it is guaranteed to see every write the constructing thread made beforehand, including all the field initializations.
In our hotel analogy, volatile is the rule that says the sign may not be flipped to "Ready" until the room is actually finished.
This is also why you can't write double-checked locking without volatile. The whole structure — check, lock, check again — only works if a non-null reference is guaranteed to point at a fully built object, and that guarantee is precisely what volatile provides. Remove it and the second check is checking against a reference that may have been published too early. There's no other keyword or trick that restores the missing guarantee for a plain mutable field; the pre-Java-5 attempts to do DCL without volatile were all either subtly broken or smuggled the synchronization back in somewhere else. On any modern JVM (Java 5 and later, where volatile's semantics were strengthened), volatile is simply part of what makes double-checked locking correct — not an optional safety belt.
So if your literal goal is "double-checked locking, just without volatile," the answer is that it can't be done safely. The good news is that you almost never need to do DCL in the first place. There's a different pattern that gives you everything DCL was reaching for — lazy, thread-safe, lock-free after the first call — with no volatile and no synchronized of your own. It isn't double-checked locking at all. It's better.
The better alternative: let the JVM do the locking for you
Here's the key insight. Everything volatile was protecting in double-checked locking — initialize the object completely before any other thread can see the reference — is something the JVM already does, perfectly, for one specific operation: class initialization.
The Java Language Specification guarantees that a class is initialized exactly once, lazily, the first time it's actively used, and that this initialization happens under an internal lock with a happens-before edge to every thread that later reads the class's fields. In other words, the classloader already contains a flawless, built-in version of the safe-publication logic that double-checked locking was hand-rolling — and you can't get it wrong, because you don't write it.
The trick is to put the singleton instance inside a separate inner class that only gets loaded when you ask for it. This is the Bill Pugh holder idiom (also called the initialization-on-demand holder):
public class LazySingleton {
private LazySingleton() {}
private static class Holder {
private static final LazySingleton INSTANCE = new LazySingleton();
}
public static LazySingleton getInstance() {
return Holder.INSTANCE;
}
}
Walk through what happens here:
When LazySingleton itself is loaded, the inner Holder class is not loaded with it — nested classes are loaded independently, on first use. So INSTANCE does not exist yet. This is what makes the pattern lazy.
The instant some thread calls getInstance() and touches Holder.INSTANCE, the JVM loads and initializes the Holder class. During that initialization it builds the singleton — and because class initialization is serialized and made visible by the JVM itself, the object is guaranteed to be fully constructed before any thread can observe the reference. The reordering problem simply cannot occur, because you never wrote the publish-the-reference step; the classloader did it correctly on your behalf.
The result is a singleton that is lazy, thread-safe, lock-free on every call, and contains no volatile and no synchronized. The synchronization is real — it's just hidden inside the JVM's class-loading machinery instead of in your code.
An even simpler option: the enum singleton
If you can live with eager initialization, the simplest correct singleton in Java is an enum, which is the approach Joshua Bloch recommends in Effective Java:
public enum Singleton {
INSTANCE;
public void doSomething() {
// your logic here
}
}
// usage:
Singleton.INSTANCE.doSomething();
Enum constants are initialized by the JVM with the same once-and-safely guarantee that powers the holder idiom, so there's no concurrency hazard. As a bonus, enums give you serialization safety and protection against reflection attacks for free — two things the other approaches need extra work to handle.
The tradeoffs: the instance is created eagerly when the enum class loads (so you lose laziness), and because the class is already an enum it can't extend another class. For a great many singletons, neither of those matters, and the enum is the cleanest thing you can write.
So when should you still use double-checked locking?
Given the holder idiom, you might wonder when plain DCL-with-volatile is ever the right call. The honest answer is: less often than its fame suggests. The holder idiom covers the overwhelming majority of "I need a lazy, thread-safe singleton" cases with less code and no memory-model reasoning.
DCL earns its place when the instance you're lazily creating depends on a runtime parameter that isn't available at class-load time — for example, a cache whose size is decided by a config value read at startup, or a per-key lazy field inside an object rather than a static singleton. The holder idiom relies on a static field initialized purely by class loading, so it can't capture runtime arguments. In those cases, double-checked locking with a volatile field is the appropriate pattern — and now you understand exactly what that volatile is buying you.
Summary
| Approach | Lazy? | Thread-safe? | Lock-free after init? | Uses volatile? |
|---|---|---|---|---|
| Naive lazy | Yes | No | — | No |
synchronized method |
Yes | Yes | No | No |
DCL without volatile |
Yes | No | Yes | No |
DCL with volatile |
Yes | Yes | Yes | Yes |
| Holder idiom | Yes | Yes | Yes | No |
| Enum | No (eager) | Yes | Yes | No |
The big takeaway: the difficulty with double-checked locking was never about locks — it was about a reference being published before the object behind it was finished. volatile fixes that by controlling instruction ordering and visibility, which is exactly why DCL can't drop it. But you rarely need to fight that battle at all: the JVM's class-initialization rules already give you safe, lazy, once-only publication for free. So instead of hand-rolling double-checked locking, reach for the holder idiom when you need laziness, the enum when eager initialization is fine, and save double-checked locking with volatile for the rarer cases where the instance genuinely depends on runtime data.