Java Singleton Pattern: Enum, Lazy Holder & Double-Checked Locking Explained
Maya's bookstore app has grown since the last post. Checkout, receipts, and nightly reports all need the same shop settings: the store name, the currency, and the tax rate. Each part of the app loads those settings its own way — checkout reads the settings file when it starts, receipts read it again when printing, reports read it a third time overnight. That worked until the day Maya changed the tax rate at noon: checkout used the new rate, receipts still used the old one, and the day's books did not reconcile. Three copies of "the settings" turned out to be three chances to disagree.
This post is about the pattern that answers a different question from the last one. Post 1, Design Patterns in Java: What They Are, When to Use Them, and When Not To, asked what varies? This post asks how many? — and when the honest answer is "exactly one," the Singleton pattern is the shape for it. The principle for this lesson: a singleton answers "how many?" — if the answer isn't "exactly one," it isn't a singleton.
1. The customer problem
Maya's requirement is plain: there must be exactly one settings loader in the whole application, every part of the app must read from it, and it must stay correct when web requests hit it from several threads at once. One loader means one file read, one set of values, and no noon-tax-rate surprises.
Notice what she did not ask for. She did not ask for a global variable, a static utility class, or "something accessible from everywhere." She asked for one property: oneness — one object, shared by everyone, created safely. Hold that thought, because "accessible from everywhere" and "exactly one" are not the same requirement, and confusing them is how singletons get a bad name. The test for everything in this post is simple: two references fetched at different times, from different threads, must always be the same object.
2. Clarify the ask: what varies, what stays
Before writing any code, split the problem again:
- What stays stable: the settings values once loaded, and the identity of the loader. Everyone reads the same object, for the whole run of the app.
- What varies: almost nothing — and that is the point. The only real questions are when the single instance is created (at startup, or on first use) and how it survives threads, serialization, and reflection.
That last clause is where most singleton tutorials stop too early. A private constructor stops new — it does not stop every way Java has of making an object. Serialization and reflection each have their own back door, and section 5 walks through both of them, on purpose, so you can see the second instance appear with your own eyes before section 6 closes the doors.
3. The minimum concept: one instance, one access point
A Singleton is a class that guarantees exactly one instance of itself exists, and provides one well-known way to reach it — usually a static method like getInstance(). It is the first creational pattern from the map in post 1: creational patterns answer "how objects are made," and this one answers "how many are made" with "one."
There are four implementations you will meet in real Java code. Here is how they differ, before we build each one that matters:
One warning before we build, carried over from post 1: a pattern is a tool, not a goal. A singleton is global state with a respectable name. If the thing you are modeling does not genuinely need to exist exactly once, a singleton buys you hidden coupling — every caller silently depends on it, and tests cannot swap it for a fake without extra seams. Section 6 ends with when not to use it, because that judgment is half the pattern.
4. Build: from many loaders to one
Here is the naive version: an ordinary class, and every part of the app creates its own loader. Settings are plain values — a store name and an integer tax percentage, so there is no floating-point rounding to argue about:
final class NaiveConfig {
private final String storeName = "Maya's Books";
private final int taxRatePercent = 8;
String storeName() { return storeName; }
int taxRatePercent() { return taxRatePercent; }
}
public class ConfigDemo {
public static void main(String[] args) {
NaiveConfig first = new NaiveConfig();
NaiveConfig second = new NaiveConfig();
System.out.println("Same object? " + (first == second));
}
} Same object? false Two loaders, two chances to disagree — exactly Maya's noon-tax-rate bug. The fix has two moves: make the constructor private so nobody else can call new, and let the class hand out its one instance through a static method. This is the eager form — the instance is created when the class loads:
final class EagerConfig {
private static final EagerConfig INSTANCE = new EagerConfig();
private final String storeName = "Maya's Books";
private final int taxRatePercent = 8;
private EagerConfig() {}
static EagerConfig getInstance() {
return INSTANCE;
}
String storeName() { return storeName; }
int taxRatePercent() { return taxRatePercent; }
} That works, and it is thread-safe for free: the JVM initializes a class exactly once, under a lock, before anyone uses it. But it pays the creation cost at startup whether the settings are ever used or not, and — as section 5 shows — it is still open to the two back doors. The form to reach for first, as a rule of thumb, is the enum singleton. An enum with a single constant is a singleton, enforced by the JVM itself:
enum AppConfig {
INSTANCE;
private final String storeName = "Maya's Books";
private final int taxRatePercent = 8;
String storeName() { return storeName; }
int taxRatePercent() { return taxRatePercent; }
}
public class EnumDemo {
public static void main(String[] args) {
AppConfig first = AppConfig.INSTANCE;
AppConfig second = AppConfig.INSTANCE;
System.out.println("Same object? " + (first == second));
System.out.println("Store: " + first.storeName() + ", tax: " + first.taxRatePercent() + "%");
}
} Same object? true
Store: Maya's Books, tax: 8% Why is the enum the default recommendation? Three guarantees come from the JVM, not from your discipline. First, the JVM creates each enum constant exactly once per classloader — no thread can race it into existence twice. Second, the JVM refuses to create enum instances reflectively, so the reflection back door in section 5 is closed by construction. Third, the JVM handles enum serialization specially: deserializing an enum constant returns the existing constant, never a fresh object — so the serialization back door is closed too. The enum form is shorter than the class form and harder to break. That combination is rare, and it is why this post recommends it first.
5. Break it: two back doors into "exactly one"
Everything above assumed the only way to make an object is new — and a private constructor blocks that way. Java has two more ways, and both defeat a plain class-based singleton. To show them, here is a serializable class singleton — the kind of settings holder a team might write before learning the enum form:
import java.io.Serializable;
final class FileConfig implements Serializable {
private static final FileConfig INSTANCE = new FileConfig();
private final String storeName = "Maya's Books";
private FileConfig() {}
static FileConfig getInstance() {
return INSTANCE;
}
String storeName() { return storeName; }
} Back door one: serialization. Deserialization does not call any constructor — it rebuilds the object from bytes, bypassing the private constructor entirely. So a round-trip through bytes produces a second, fully formed instance:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SerializationBreak {
public static void main(String[] args) throws Exception {
FileConfig original = FileConfig.getInstance();
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(original);
}
FileConfig copy;
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
copy = (FileConfig) in.readObject();
}
System.out.println("Same object after round-trip? " + (original == copy));
}
} Same object after round-trip? false Two settings loaders again — one of them invisible to every getInstance() call. If the app ever caches settings to disk, or passes them across a network, the singleton silently stops being single.
Back door two: reflection. Reflection can open a private constructor and call it anyway. In a plain single-module program like this one, that is one setAccessible call away:
import java.lang.reflect.Constructor;
public class ReflectionBreak {
public static void main(String[] args) throws Exception {
FileConfig original = FileConfig.getInstance();
Constructor<FileConfig> hidden = FileConfig.class.getDeclaredConstructor();
hidden.setAccessible(true);
FileConfig copy = hidden.newInstance();
System.out.println("Same object after reflection? " + (original == copy));
}
} Same object after reflection? false This one is less likely to happen by accident — nobody reflectively constructs a settings loader in normal code — but it matters for the same reason the serialization break matters: "exactly one" is a guarantee you are making to every caller, and a guarantee with two known holes is not a guarantee. It is a hope. Section 6 closes both holes, and then covers the two remaining implementation choices.
6. Harden it: close the doors, then choose your form
Fix one: readResolve against serialization. If deserialization finds a readResolve method, it returns that method's result instead of the freshly rebuilt object. So the class hands back its one instance every time:
// Called by deserialization instead of returning the fresh object.
private Object readResolve() {
return INSTANCE;
} Add that method to FileConfig and the serialization demo prints Same object after round-trip? true — the twin is discarded before anyone can hold a reference to it. As a matter of production hygiene, a serializable singleton should also declare a serialVersionUID, so later harmless edits to the class do not break previously stored bytes.
Fix two: a constructor guard against reflection. Refuse any construction after the first one:
private FileConfig() {
// The legitimate creation runs while INSTANCE is still null;
// any later attempt — reflection or otherwise — is refused.
if (INSTANCE != null) {
throw new IllegalStateException("already created");
}
} With this guard, the reflection demo no longer returns a twin — the reflective call fails instead. Be honest about what the guard is: it stops the ordinary reflection attack, not every conceivable back door. A determined caller with low-level tools can still bypass constructors entirely. That is one more reason the enum form is the default — it does not need guards, because the JVM itself refuses both attacks.
Lazy, without locks: the lazy-holder idiom. If creating the instance is expensive and might never be needed, the eager form wastes startup time. The holder idiom gets laziness and thread safety from the JVM's class-loading lock, with no synchronized anywhere:
final class LazyConfig {
private final String storeName = "Maya's Books";
private final int taxRatePercent = 8;
private LazyConfig() {}
// Holder loads only when getInstance() first touches it —
// and the JVM initializes a class exactly once, under a lock.
private static final class Holder {
static final LazyConfig INSTANCE = new LazyConfig();
}
static LazyConfig getInstance() {
return Holder.INSTANCE;
}
String storeName() { return storeName; }
int taxRatePercent() { return taxRatePercent; }
} Lazy, with a lock you control: double-checked locking. You will meet this form in real codebases, so understand it even if you rarely write it. The idea: check without locking (the fast path), lock only if the instance is missing, check again inside the lock, then create:
final class DclConfig {
private static volatile DclConfig instance;
private final String storeName = "Maya's Books";
private DclConfig() {}
static DclConfig getInstance() {
DclConfig local = instance;
if (local == null) {
synchronized (DclConfig.class) {
local = instance;
if (local == null) {
instance = new DclConfig();
}
}
}
return local;
}
String storeName() { return storeName; }
} The word volatile is not decoration — without it, this code is broken, and the reason is worth understanding. Creating an object is three steps: allocate memory, run the constructor, and store the reference. Without volatile, the JVM and the processor are allowed to reorder those steps so the reference is stored before the constructor finishes. A second thread can then read a non-null reference to a half-built object — its fields still at their defaults, so storeName would read as null — and proceed to use it. volatile forbids that reordering: the write of the reference happens only after the constructor completes, and any thread that reads a non-null volatile reference sees the fully constructed object. The local-variable read on the fast path is a small optimization so the common case touches the volatile field only once. If you remember one sentence about double-checked locking, make it this one: without volatile, double-checked locking can hand out an object whose constructor has not finished.
When not to use a singleton at all. Three honest costs, stated plainly. First, testing: global state leaks between tests, and a test that needs different settings cannot substitute a fake without extra seams — if your tests need different values, pass the settings in as a parameter instead. Second, hidden coupling: every caller silently depends on the global, so changing its behavior ripples everywhere with no call site to point at. Third, scope: "one per JVM" is the wrong shape when the real requirement is one per request, one per tenant, or one per test. As a rule of thumb, reach for a singleton only when "exactly one" is required by the problem itself — like Maya's shared shop settings — and prefer passing the dependency in everywhere else. That preference has a name, dependency injection — for now, the rule of thumb is enough: singleton for genuine oneness, parameter for everything else.
Finally, verify with explicit checks, not assertions — the same discipline as post 1. Assertions can be switched off at runtime, so they are documentation, not a safety net. A small check program that compares references with == and throws on mismatch catches a broken singleton the same way post 1's checks caught bad discounts: loudly, and whether or not assertions are enabled.
7. Communicate: explain it to a human
The code is only half the job. Here is how you explain this change to Maya, without jargon:
"Before, every part of the app kept its own copy of the shop settings, so changing the tax rate at noon meant checkout and receipts disagreed. Now there is exactly one settings book, and everyone reads from it — checkout, receipts, reports, all of them. It is created once, it cannot be copied by saving and reloading it, and it is safe when several web requests read it at the same time. The one trade-off to know: because there is only one, tests that need different settings will pass them in directly instead of swapping the book — that keeps the real app simple and the tests honest."
If she understands that paragraph, she understands the design — and she can approve the next change with confidence, because she knows exactly what "one settings book" guarantees and what it costs.
Field check
Six rules to carry into the rest of the track:
- A singleton answers "how many?" If the answer isn't "exactly one," it isn't a singleton — it is global state looking for an excuse.
- Enum is the default. The JVM creates the constant exactly once, refuses reflective creation, and returns the same constant on deserialization. Shorter code and stronger guarantees is a rare combination — take it.
- Lazy holder gives you lazy plus thread-safe with no locks. The JVM's class-initialization lock does the work; your code stays simple.
- Double-checked locking needs volatile. Without it, another thread can observe a half-built object whose constructor has not finished.
- Back doors are part of the design. Serialization and reflection each create a second instance unless you close them — with
readResolveand a constructor guard — or choose enum, which closes both. - A singleton is global state — budget its testing cost. When tests or tenants need different values, pass the dependency in instead of reaching for the global.
Previous in this track: Design Patterns in Java: What They Are, When to Use Them, and When Not To · Continue: Java Learning Roadmap 2026
Comments
Post a Comment