1.3 Singleton
The Singleton pattern guarantees that a class has exactly one global instance and provides a single access point to it. Configuration centers, loggers, connection pools, and caches are often designed as singletons.
It looks trivial to implement, yet it hides more traps than almost any other pattern: lazy initialization, thread safety, serialization, reflection attacks — and the most fundamental controversy of all: a singleton is essentially global state.
The shared settings lab below focuses on one visible effect: recolor only the login page and see whether the other pages change too. If they all change, they share one instance; if only login changes, each page has its own instance.
Three common implementations
// 1) Eager: created at class load. Simple and thread-safe, but not lazy.
class Eager {
private static final Eager INSTANCE = new Eager();
private Eager() {}
public static Eager getInstance() { return INSTANCE; }
}
// 2) Static holder idiom: lazy + thread-safe. Recommended.
class Lazy {
private Lazy() {}
private static class Holder { static final Lazy INSTANCE = new Lazy(); }
public static Lazy getInstance() { return Holder.INSTANCE; }
}
// 3) Enum: most concise, naturally safe against reflection and serialization
// (recommended by Effective Java).
enum Config { INSTANCE; }volatile and get bitten by instruction reordering. On the JVM, the holder idiom or an enum is almost always the better choice.Why the "unsynchronized lazy" version breaks
The classic bug is that "check then create" is not atomic. Two threads can:
1. Thread A evaluates instance == null as true.
2. Thread B also evaluates instance == null as true (A hasn't assigned yet).
3. Each thread news its own object — the singleton is broken, and there are now two instances.
The two-thread race scheduler below lets you interleave the threads by hand, watch the "no lock" path advance into two local objects, then turn on the synchronized lock and see the critical section block the race.
The real debate: is Singleton an anti-pattern?
Many senior engineers are wary of singletons — not because of "uniqueness" itself, but because it is usually used as a global variable:
- Dependencies are hidden. Any code can call
Config.getInstance(), so you cannot tell from a function's signature what it depends on.
- Testability suffers. Singleton state leaks across test cases and is hard to replace with a mock.
- Concurrency and lifecycle get complicated. Who destroys it? How do you handle state races across threads?
The more modern approach: keep the "only one instance" constraint, but inject it via dependency injection instead of having code actively call getInstance() everywhere. You keep uniqueness while making dependencies explicit and replaceable.