Double-checked locking in Java is a concurrency pattern meant to lazily initialize a singleton without synchronizing every access, but it is broken without a volatile field. The volatile keyword fixes the visibility and reordering problems that can cause one thread to see a partially constructed object, but it does not fix every concurrency issue—you still need to understand what it does and does not guarantee.
Background: The Singleton Problem and Lazy Initialization
Singletons are objects that should exist only once per JVM. The simplest way is to create it eagerly at class load time:
class Singleton {
static final Singleton INSTANCE = new Singleton();
private Singleton() {}
}
That is always safe, but if constructing the object is expensive and the singleton might never be used, you might want to defer creation until the first call. That is lazy initialization. The naive thread-safe version synchronizes the entire method:
static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
That works but adds a lock acquisition on every call, even after the object is already created. Double-checked locking tries to avoid that cost by checking the field once without a lock, then taking the lock only if it is still null, and checking again inside the lock.
What Double-Checked Locking Looks Like
The classic (broken) pattern looks like this:
class Singleton {
private static Singleton instance;
static Singleton getInstance() {
if (instance == null) { // first check (no lock)
synchronized (Singleton.class) {
if (instance == null) { // second check (inside lock)
instance = new Singleton();
}
}
}
return instance;
}
}
The idea is sound: most calls see a non-null instance and skip the lock. The problem is that Java’s memory model does not guarantee that the instance field will be visible to other threads immediately, nor that the writes inside the constructor won’t be reordered with the write to instance.
Why It Breaks Without volatile: Reordering and Visibility
In the JVM, new Singleton() is not a single atomic operation. It roughly does three things:
- Allocate memory for the object
- Call the constructor to initialize fields
- Assign the reference to the variable
In theory, the compiler or CPU can reorder the second and third steps. If thread A writes the reference to instance before the constructor finishes, thread B can see a non-null instance and immediately return it, but the object’s fields are still at their default values (0, null, false). That is a partially constructed object.
Even if the reordering never happens on a particular CPU, the Java Memory Model does not guarantee that thread B will see the updated reference at all unless there is a happens-before relationship. Without synchronization or a volatile field, thread B might keep seeing null forever, or see a stale value.
What volatile Actually Fixes
Declaring the field volatile fixes both problems:
private static volatile Singleton instance;
When you write to a volatile field, the JVM inserts memory barriers that prevent the constructor writes from being reordered after the volatile write. When you read a volatile field, the JVM ensures that any writes that happened before the volatile write in thread A are visible to thread B. This creates a happens-before relationship: the constructor’s writes happen-before the volatile write, and the volatile write happens-before any subsequent volatile read that sees the new value.
So with volatile, the double-checked locking pattern is safe for lazy initialization of a singleton. This is the most common fix, and it is what the pattern is usually taught with.
What volatile Does NOT Fix
volatile only guarantees visibility and ordering for the specific field you mark. It does not make the entire object thread-safe. If the singleton has mutable state that is read and written by multiple threads after initialization, volatile on the instance field does nothing to protect that state. You still need synchronization, atomic variables, or immutable fields.
Also, volatile does not make the initialization atomic. If two threads both see instance == null and both enter the synchronized block, the second check inside the lock prevents creating two instances. That is the second check’s job, not volatile’s. If you omit the second check, volatile alone will not prevent multiple instances.
Another limitation: volatile does not help if the constructor itself publishes this (for example, by passing it to another object). The memory barriers only order writes to the volatile field; if the constructor leaks the reference before the volatile write, other threads can still see a partially constructed object.
Alternatives and Modern Practice
While double-checked locking with volatile works, it is no longer the recommended way to create a singleton in modern Java. Simpler and safer alternatives exist:
- Eager initialization – just create it at class load. This is always safe and often good enough.
- Static inner class (initialization-on-demand holder idiom) – the JVM loads the inner class only when you reference it, which gives lazy initialization without any synchronization:
class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
static Singleton getInstance() {
return Holder.INSTANCE;
}
}
- Enum singleton –
enum Singleton { INSTANCE }is a concise, serialization-safe way to implement a singleton. - Use a framework – dependency injection containers (like Spring) manage singleton scope for you, so you rarely need to write this pattern yourself.
If you do use double-checked locking, make sure the field is volatile and that you are on Java 5 or later (the memory model was fixed in Java 5). On older JVMs, even volatile did not fully fix the pattern.
Common Misconceptions and Pitfalls
One common misconception is that synchronized alone is enough. It is not, because the first check is outside the lock. The first check can read a stale value, and even if it reads a non-null value, that value might be a partially constructed object. The synchronized block only protects the second check and the creation, not the first read.
Another pitfall is using double-checked locking for non-singleton objects or for fields that are not immutable. The pattern is only safe for immutable or effectively immutable objects. If the object has mutable fields that are written after construction, you need additional synchronization for those writes.
There is also the question of whether the performance benefit is worth it. In modern JVMs, uncontended synchronization is very cheap, and the JIT can optimize away some locks. For most applications, the difference between double-checked locking and a simple synchronized method is negligible. Premature optimization is rarely worth the added complexity and risk.
Frequently Asked Questions
Is double-checked locking still broken in Java?
No, if the field is declared volatile and you are on Java 5 or later, the pattern is safe. The original problem was due to the old memory model; the fix is well understood and documented.
Why do I need the second check inside the synchronized block?
The second check ensures that only one instance is created when multiple threads race to initialize. Without it, two threads could both pass the first check, then one creates the instance, releases the lock, and the other enters the lock and creates a second instance.
Can I use double-checked locking for a non-singleton object?
You can, but it only makes sense for lazy initialization of a shared object. For a regular object that each thread creates independently, you do not need any synchronization.
What is the difference between volatile and synchronized?
volatile provides visibility and ordering guarantees for a single field, but it does not provide mutual exclusion. synchronized provides mutual exclusion and also creates a happens-before relationship, but it is heavier. They are not interchangeable; they solve different problems.
Is the static inner class idiom better than double-checked locking?
Yes, for most cases. It is simpler, has no synchronization overhead, and is safe by the JVM’s class initialization rules. It is the recommended approach for lazy singletons in modern Java.
Does volatile work on all JVM versions?
The current Java Memory Model (JSR-133) was introduced in Java 5. On Java 1.4 and earlier, volatile did not guarantee the same ordering, so double-checked locking was still unsafe even with volatile. If you are on a modern JDK, you are fine.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- Jenkins vs GitHub Actions: Which CI/CD Fits Your Team?
- Turn Off Backup Encryption in iTunes?
- Campus Tech Innovation Hubs: What They Are
- Poco C51 Airtel Prepaid Locked Variant India?
- Shared VPS Hosting: Is It Better?
- Disk Defrag Pro: What It Does and Whether You Need It