Every enterprise Java codebase has a few objects that "should exist only once": a configuration holder, a connection pool, a product catalog cache, a logger. The Singleton pattern is the classic way to guarantee that, and it is also the design pattern that gets written wrong most often, because its bugs only show up when several threads touch it at the same time. This article compares six ways to implement the Singleton pattern in Java, verifies them with 16 real threads, uses the Java Memory Model (JMM) to explain why double-checked locking needs volatile, and shows three ways to break the Singleton pattern: serialization, reflection and multiple ClassLoaders. All code compiles with --release 17 (the Java 17 LTS language and API) and was actually run, on a 16 GB machine running macOS.

If you have not read the underlying principles yet, start with SOLID principles in Java with practical examples. The end of this article returns to the question of why the Singleton pattern so often violates SOLID.

What problem the Singleton pattern solves

According to the Singleton pattern page on Refactoring.Guru, the intent of this pattern is to ensure a class has only one instance and to provide a global access point to it. The structure has three parts: a private constructor so nobody can call new from outside, a static field that holds the single instance, and a static method (usually getInstance()) that returns it. In multithreaded code, creating the instance must be protected, and that is where all the trouble starts.

One thing to state clearly up front: in Java, "one instance" always means one instance per ClassLoader. The Spring Framework draws this distinction clearly in its bean scopes documentation: the GoF Singleton pattern fixes the scope at one instance per ClassLoader, while a Spring singleton is one instance per container and per bean definition. The two concepts share a name but are not the same.

Six Singleton pattern implementations and the results with 16 threads

To compare the Singleton pattern implementations fairly, each one has a "slow" constructor (it sleeps 50 ms to simulate reading configuration) and counts how many times the constructor actually runs. The test harness starts 16 threads at the same moment with a CountDownLatch; each thread calls getInstance() and records the identityHashCode of the object it receives:

static int distinct(Callable<Object> getter, int threads) throws Exception {
    ExecutorService pool = Executors.newFixedThreadPool(threads);
    CountDownLatch start = new CountDownLatch(1);
    Set<Integer> ids = ConcurrentHashMap.newKeySet();
    for (int i = 0; i < threads; i++) {
        pool.submit(() -> {
            start.await();
            ids.add(System.identityHashCode(getter.call()));
            return null;
        });
    }
    start.countDown();
    pool.shutdown();
    pool.awaitTermination(10, TimeUnit.SECONDS);
    return ids.size();
}

1. Eager initialization

Create the instance as soon as the class is loaded. It is simple and thread-safe thanks to the JVM's class initialization mechanism; the drawback is that the instance is created even if nobody uses it:

static class Eager {
    private static final Eager INSTANCE = new Eager();
    private Eager() { slowInit(); }
    static Eager getInstance() { return INSTANCE; }
}

2. Lazy and not thread-safe (wrong)

static class LazyUnsafe {
    private static LazyUnsafe instance;
    private LazyUnsafe() { slowInit(); }
    static LazyUnsafe getInstance() {
        if (instance == null) instance = new LazyUnsafe();
        return instance;
    }
}

Two threads both see instance == null and both call new. This is the classic "check-then-act" race condition.

3. Lazy with a fully synchronized method

static class LazySync {
    private static LazySync instance;
    private LazySync() { slowInit(); }
    static synchronized LazySync getInstance() {
        if (instance == null) instance = new LazySync();
        return instance;
    }
}

Correct, but every call, even after initialization has finished, has to go through the lock. On modern Java the cost is small when uncontended, but on a hot path with many threads it is still a potential bottleneck.

4. Double-checked locking with volatile

static class Dcl {
    private static volatile Dcl instance;
    private Dcl() { slowInit(); }
    static Dcl getInstance() {
        Dcl s = instance;               // a single volatile read
        if (s == null) {
            synchronized (Dcl.class) {
                s = instance;
                if (s == null) instance = s = new Dcl();
            }
        }
        return s;
    }
}

5. Initialization-on-demand holder

static class Holder {
    private Holder() { slowInit(); }
    private static class H { static final Holder INSTANCE = new Holder(); }
    static Holder getInstance() { return H.INSTANCE; }
}

6. Enum singleton

enum EnumSingleton {
    INSTANCE;
    EnumSingleton() { slowInit(); }
}

Actual run results:

$ java -cp out Variants
Eager        16 threads -> 1 distinct objects, constructor ran 1 time
LazyUnsafe   16 threads -> 14 distinct objects, constructor ran 16 times
LazySync     16 threads -> 1 distinct objects, constructor ran 1 time
Dcl          16 threads -> 1 distinct objects, constructor ran 1 time
Holder       16 threads -> 1 distinct objects, constructor ran 1 time
Enum         16 threads -> 1 distinct objects, constructor ran 1 time

The numbers for the second implementation (lazy, not thread-safe) vary between runs: I ran it 8 times in a row and saw between 13 and 16 distinct objects, while the constructor always ran all 16 times (the table above is one run).

The Eager row also gives exactly 1 object: in Java, "eager" means initialization happens in the class initialization step, and the JVM guarantees that step runs once, thread-safely, the first time the class is used. The object count is off because a thread can read an instance that another thread has just overwritten in the instance field; what matters is that the constructor ran 16 times instead of 1, which means this is no longer a Singleton pattern at all. The other four implementations are correct under this load. One important caveat: a clean run does not prove the code is correct, especially for DCL without volatile. The next section explains why you have to rely on the specification rather than on experiments.

The Java Memory Model and why volatile is mandatory in DCL

The Java Language Specification defines the happens-before relation in JLS 17.4.5. If action x happens-before action y, then x is visible to and ordered before y. The rules to remember are program order within a single thread; an unlock of a monitor happens-before every subsequent lock of that same monitor; and a write to a volatile field happens-before every subsequent read of that field. Two conflicting accesses (at least one of them a write) with no happens-before relation form a data race, and when there is a data race, the JMM does not guarantee which value a reading thread sees.

Now consider DCL without volatile:

static class Broken {
    private static Broken instance;     // NOT volatile
    int value;
    private Broken() { value = 42; }
    static Broken get() {
        if (instance == null) {                     // (1) unsynchronized read
            synchronized (Broken.class) {
                if (instance == null) instance = new Broken();
            }
        }
        return instance;
    }
}

The statement instance = new Broken() consists of three logical steps: allocate memory, run the constructor (write value = 42), then assign the reference to instance. The compiler or CPU is allowed to move the reference assignment before the write to value, because within a single thread the result is the same. Another thread running line (1) can then see instance != null and return an object that is not fully constructed, reading value == 0. Line (1) is a read with no happens-before relation to the write inside the synchronized block, in other words a data race.

Add volatile to instance and the volatile write happens-before every later volatile read, and the writes in the constructor come before the volatile write in program order, so they are visible to the reading thread as well. That is why the DCL implementation in section 4 is correct, and why it uses the local variable s to perform only one volatile read on the fast path.

I ran the Broken snippet on my machine:

$ java -cp out Jmm
Broken DCL: 42 (the JMM does not guarantee 42 for an unsynchronized read)

The result is 42, and that is exactly the point to understand: this version was called from a single thread, so it cannot reproduce the bug. Even with several threads, instruction reordering depends on the JIT and the CPU architecture (ARM has a weaker memory ordering than x86), so the bug may never appear on your machine yet show up in another environment. Code that "works" is not evidence of correctness, so only the happens-before rules of the specification can be trusted.

In addition, JLS 17.5 has a separate rule for final fields: if all fields of an object are final and the constructor does not leak this, any thread that sees the reference also sees the correct values of those fields. This explains why immutable objects are safe to share, but do not rely on it as a substitute for volatile on an object with mutable state.

Why the holder idiom and enum need no lock

The last two Singleton pattern implementations, holder and enum, use neither synchronized nor volatile in your code and are still thread-safe, because they rely on the JVM's class initialization mechanism described in JLS 12.4.2. Each class has its own initialization lock; if one thread is initializing a class, any other thread that needs it waits until initialization completes, and the completion of initialization happens-before every later access. Per JLS 12.4.1, a class is initialized only right before the first use of a non-constant static field, the first call of a static method, or the first creation of an instance.

With the holder idiom, Holder.H is initialized only when getInstance() accesses H.INSTANCE for the first time. That is lazy initialization for free, and safe. If the constructor throws an exception, class H moves to an erroneous state: the first access gets ExceptionInInitializerError and later accesses get NoClassDefFoundError. Keep this in mind when diagnosing startup failures: the second error often hides the root cause, so look for the first error in the log.

An enum is the shortest way to write a Singleton pattern and is recommended by Joshua Bloch in Effective Java (the item "Enforce the singleton property with a private constructor or an enum type"): the JVM guarantees each enum constant has exactly one instance, even under serialization or reflection attacks, as the next section shows.

Three ways to break the Singleton pattern

A Singleton pattern built as "private constructor, static field" looks solid but has three escape routes. The following code tries all three, with a fix for each:

public static class Config implements Serializable {
    private static final Config INSTANCE = new Config();
    final String env = "prod";
    private Config() {}
    public static Config getInstance() { return INSTANCE; }
}
static class ConfigFixed implements Serializable {
    private static final ConfigFixed INSTANCE = new ConfigFixed();
    private ConfigFixed() {
        if (INSTANCE != null) throw new IllegalStateException("Already initialized");
    }
    static ConfigFixed getInstance() { return INSTANCE; }
    private Object readResolve() { return INSTANCE; }
}
enum Registry { INSTANCE }
$ java -cp out Breakers out
Serialization, no readResolve    : same=false
Serialization, with readResolve  : same=true
Serialization, enum              : same=true
Reflection into private constructor: same=false
Reflection with guard          : java.lang.IllegalStateException: Already initialized
Reflection into enum             : java.lang.IllegalArgumentException: Cannot reflectively create enum objects
Two ClassLoaders                 : same=false, class.same=false
  • Serialization: writing and then reading back a Serializable object creates a new copy, so same=false. Adding a readResolve() method that returns INSTANCE fixes it. An enum needs nothing: the Java Object Serialization Specification says an enum constant is serialized as its name only, and on deserialization the JVM calls Enum.valueOf, so you always get the original constant back.
  • Reflection: setAccessible(true) can open a private constructor and create a second instance (same=false). You can block it by throwing an exception in the constructor if INSTANCE already exists, which works for an eager singleton. Enums are blocked out of the box: Cannot reflectively create enum objects. Since Java 17, JDK modules also strongly encapsulate JDK-internal classes against setAccessible (JEP 403), but your own application classes remain open.
  • Multiple ClassLoaders: I loaded the same Config class with two separate URLClassLoader instances; the result was two objects and two classes with different identities (class.same=false). This happens for real in application servers and plugin systems, where each application has its own ClassLoader. A "singleton" is then unique only within each ClassLoader.

The Singleton pattern in the JDK and in Spring

The JDK has a familiar example of the Singleton pattern in java.lang.Runtime.getRuntime(), which returns the one Runtime instance per Java process. In a Spring Boot application, however, you almost never write a Singleton pattern yourself anymore. Beans have singleton scope by default, and the container creates one instance per bean definition and injects it where needed through the constructor. This removes hidden global state, lets you replace the bean with a mock in tests, and still achieves the goal of "only one instance". But a Spring singleton bean is shared by every request thread, so it must not hold per-request mutable state in instance fields; that is a very common source of race conditions in web applications.

When not to use the Singleton pattern

The Singleton pattern is often called an "anti-pattern" for specific reasons, and Refactoring.Guru lists them too: it takes on both controlling the number of instances and business logic (violating the Single Responsibility principle in SOLID), it hides dependencies because code calls Config.getInstance() from anywhere, and it is hard to test because it cannot be replaced with a mock object. A few practical suggestions:

  • Global mutable state (counters, writable caches) is where the Singleton pattern does the most harm: one test pollutes another. If you must, allow a reset in tests or inject the instance instead of calling the static method.
  • If you have a DI container: do not hand-write a Singleton pattern; declare a singleton bean and inject it through the constructor instead of implementing getInstance() yourself.
  • If you need one instance per tenant or per user: that is not a Singleton; use a Map of instances or a factory (see the next article on Factory Method).
  • Stateless objects: the Singleton pattern gives no benefit; if there is no state to hold, calling a static method or creating a new object is still cheap, so there is no need to "singletonize" it.

Which Singleton pattern implementation to choose

ApproachLazyThread-safeSerialization-proofReflection-proofComplexity
EagerNoYesNeeds readResolveNeeds a guardLow
Unsynchronized lazyYesNoNeeds readResolveNeeds a guardLow, but wrong
Fully synchronized methodYesYesNeeds readResolveNeeds a guardLow
DCL with volatileYesYesNeeds readResolveNeeds a guardHigh
Holder idiomYesYesNeeds readResolveNeeds a guardLow
EnumYes (on first access)YesBuilt inBuilt inLowest

Recommendation when choosing a Singleton pattern: use an enum when the Singleton pattern does not need to extend another class; use the holder idiom when you need laziness and must extend a class; use DCL only when you understand the reasons and have a legitimate one. In a Spring application, prefer a container-managed singleton bean.

Common mistakes

  1. DCL without volatile: a rare bug that is hard to reproduce and is correct only "by accident" on some CPUs.
  2. A constructor that does heavy work or can throw in a holder or eager singleton: the class initialization failure turns into NoClassDefFoundError on later calls, which is very hard to read.
  3. A Singleton pattern that holds mutable state without synchronization: several threads writing at once corrupt the data; use thread-safe structures such as ConcurrentHashMap or keep the state immutable.
  4. Forgetting readResolve when a Singleton pattern class implements Serializable (often because it inherits some interface).
  5. Confusing a Spring singleton with the GoF Singleton pattern: two ClassLoaders or two containers give two instances.
  6. Treating a single test run as proof: a green run does not prove memory-model correctness; reason with happens-before or use a tool such as jcstress.

Summary

  • The Singleton pattern guarantees a single instance within one ClassLoader, not across the whole JVM.
  • Unsynchronized lazy creates 16 objects in 16 threads; synchronized, DCL with volatile, holder and enum all give exactly 1.
  • volatile in DCL is there to establish happens-before between the reference write and the writes inside the constructor (JLS 17.4.5).
  • Serialization, reflection and ClassLoaders can all create a second instance; an enum blocks the first two routes but not the ClassLoader one.
  • In a Spring application, use a singleton bean and dependency injection instead of implementing the Singleton pattern with a static getInstance().

The full source code (Variants, Breakers, Jmm) compiles with javac --release 17 -d out *.java and runs with java -cp out Variants, java -cp out Breakers out and java -cp out Jmm.

Next article

Read next: Factory Method and Abstract Factory: separating object creation from logic.