Singleton Pattern: One Instance, and the Ways It Goes Wrong
What a singleton guarantees, four Java versions tested with 8 racing threads (the naive one made 8 objects), when to pick the enum or holder version, and why tests dislike singletons.
The singleton pattern guarantees one instance of a class through a private constructor and a static access method. Interviewers expect thread safety: a lazy version without locking can create several objects when threads race past the null check. Synchronized access, double-checked locking with volatile, the holder idiom and an enum each fix that, with different trade-offs.
A singleton guarantees one instance of a class and a global way to reach it. The idea is simple and the implementation is easy to get wrong under threads: a lazy version without locking created 8 separate objects when 8 threads called it at once. Synchronized, double-checked locking, the holder idiom and an enum all fix that, each with different trade-offs.
- A lazy Java singleton without locking created 8 distinct objects when 8 threads called get() at once with a 50 ms sleep in the constructor.
- A synchronized get() method produced exactly one singleton instance in the 8-thread test, at the cost of taking the lock on every later call.
- Double-checked locking needs the instance field to be volatile, or another thread may see a non-null reference to an object whose fields are not fully written.
- The initialisation-on-demand holder idiom is lazy and thread-safe without explicit locking, because the JVM runs class initialisation once when the nested class is first used.
- An enum singleton resists serialization and reflection attacks, because the JVM creates enum constants once and code cannot call an enum's constructor.
- Dependency injection keeps a single shared instance created at startup while making each class declare what it needs, which avoids the testing problems of a global singleton.
01What a singleton is for
A singleton has a private constructor and a static method that returns the one instance. Typical uses are things where two copies would disagree: a configuration holder, a connection pool, a logger, a cache. Everything calls Config.get() and sees the same object.
The pattern has two jobs that are worth separating. One is controlling creation, so only one object is ever made. The other is giving global access to it. The first is often right. The second is where most of the criticism comes from, and we come back to it at the end.
02Attempt 1: lazy and unsafe
static class Naive {
private static Naive instance;
private Naive() { /* slow setup */ }
static Naive get() {
if (instance == null) instance = new Naive();
return instance;
}
}On one thread this works. With several threads, two can both see instance == null before either assigns it, and each builds its own object. To make the race visible I added a 50 ms sleep inside the constructor and called get() from 8 threads released at the same moment. The result was distinct objects=8, constructor calls=8. All eight threads got a different object, so the "singleton" was not one. Without the sleep the window is tiny and the bug appears rarely, which is exactly why it survives testing and fails in production.
03Fix 1: synchronize the method
static synchronized Locked get() {
if (instance == null) instance = new Locked();
return instance;
}Same test, same 8 threads: distinct objects=1, constructor calls=1. Only one thread at a time can be inside get(), so the check and the creation cannot interleave. The cost is that every later call also takes the lock, even though the instance exists. For code called millions of times that adds contention. For most applications it is fine, and it is the easiest version to reason about.
04Fix 2: double-checked locking
private static volatile Dcl instance;
static Dcl get() {
Dcl r = instance;
if (r == null) {
synchronized (Dcl.class) {
r = instance;
if (r == null) instance = r = new Dcl();
}
}
return r;
}Result: distinct objects=1, constructor calls=1. After creation the fast path is a single read with no lock. The volatile keyword is not decoration. Without it, the compiler or CPU may let another thread see the reference before the object's fields are fully written, so it could use a half-built object. This is why older copies of this pattern that omit volatile are broken. Since Java 5 the memory model makes the volatile version correct.
05Fix 3: initialisation-on-demand holder
private static class H { static final Holder INSTANCE = new Holder(); }
static Holder get() { return H.INSTANCE; }Result: distinct objects=1, constructor calls=1. The nested class H is not loaded until get() first touches it, and the JVM guarantees class initialisation runs once and safely across threads. So it is lazy, thread-safe and has no explicit locking, with very little code. For a lazily created singleton in Java this is a good default.
06Fix 4: the enum singleton
enum EnumSingleton { INSTANCE; int hits; }
EnumSingleton.INSTANCE == EnumSingleton.valueOf("INSTANCE") // trueThe JVM creates enum constants once and handles thread safety. It also resists the two ways other versions are broken on purpose: serialization (a normal singleton can come back as a second object after deserializing unless you add readResolve) and reflection (code can call a private constructor, but not an enum's). The limits are that an enum cannot extend another class and its instance is created when the enum class is first initialised (on first use, like the holder idiom), so a constructor that fails will throw ExceptionInInitializerError at that point.
07The real downsides
Because any code can call Config.get(), a class's dependence on it is not visible in its constructor or method signatures. That makes code harder to read and reuse. It is also hard to test: state in the singleton persists between tests, and you cannot easily swap in a fake.
The usual alternative is dependency injection. Create the object once at the top of the program, then pass it to the classes that need it. You still get a single instance, but each class declares what it needs and tests can pass a substitute. Frameworks such as Spring do this by default with singleton scope, which is the same "one instance" idea without the static global.
Use a classic singleton sparingly, for real process-wide resources. Prefer passing the object in when you can.
08Common interview questions
Q: Why is lazy initialisation without a lock unsafe?
A: Two threads can both pass the null check before either assigns, so two instances are created.
Q: Why does double-checked locking need volatile?
A: Without it, another thread may see a non-null reference to an object whose constructor has not finished writing its fields.
Q: Easiest thread-safe singleton in Java?
A: An enum with one constant, or the holder idiom for lazy creation.
Q: How can serialization or reflection break a singleton?
A: Deserializing creates a new object unless readResolve returns the existing one, and reflection can invoke a private constructor. An enum avoids both.
Q: Why do people dislike singletons?
A: Global state, hidden dependencies and difficult testing.
Q: What replaces it?
A: Dependency injection with a single shared instance created at startup.