Thread-Safe Singleton Patterns in Java

Singleton Pattern Overview

The singleton pattern ensures that a class has exactly one instance and provides a global point of access to it. This is especial valuable when managing shared resources—such as data base connection pools, configuration managers, or large in-memory caches—where duplication would waste memory or cause inconsistent state.

Two primary implementation strategies exist: eager initialization ("eager singleton") and lazy initialization ("lazy singleton"). Each differs in when the instance is created and how thread safety is handled.

Eager Initialization

In this approach, the instance is created at class loading time—before any thread requests it. Since static initialization occurs once and is inherently thread-safe in JVM specification, no synchronization is needed during retrieval.

public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

The private constructor prevents external instantiation, and the final modifier guarantees immutability of the reference.

Lazy Initialization with Double-Checked Locking

Lazy initialization defers object creation until the first call to getInstance(). While more memory-efficient for rarely used objects, it introduces race conditions without proper synchronization.

A naive implementation:

public class NaiveLazySingleton {
    private static NaiveLazySingleton instance;

    private NaiveLazySingleton() {}

    public static NaiveLazySingleton getInstance() {
        if (instance == null) {
            instance = new NaiveLazySingleton(); // Not thread-safe!
        }
        return instance;
    }
}

This fails under concurrency: two threads may both pass the null check and instantiate separate objects.

To fix this, use double-checked locking with volatile:

public class ThreadSafeLazySingleton {
    private static volatile ThreadSafeLazySingleton instance;

    private ThreadSafeLazySingleton() {}

    public static ThreadSafeLazySingleton getInstance() {
        if (instance == null) { // First check (no lock)
            synchronized (ThreadSafeLazySingleton.class) {
                if (instance == null) { // Second check (with lock)
                    instance = new ThreadSafeLazySingleton();
                }
            }
        }
        return instance;
    }
}

The volatile keyword serves two critical purposes:

  • Ensures visibility: writes to instance are immediately visible to all threads.
  • Prevents instruction reordering: blocks the JVM from moving the assignment (instance = ...) before the object's construction completes—avoiding publication of a partially initialized object.

When to Use Each Pattern

  • Eager singleton: Prefer when initialization cost is low, or when the instance is always needed early (e.g., application-wide logger, metrics collector).
  • Lazy singleton with double-checked locking: Choose when initialization is expensive and usage is conditional (e.g., loading a large configuration file only if a feature is enabled).

Avoid reflection-based instantiation in production code—it breaks encapsulation and defeats the purpose of singleton enforcement. If such bypassing must be supported (e.g., for testing), consider explicit APIs rather than exposing constructors.

Tags: java multithreading design-patterns singleton Concurrency

Posted on Sat, 23 May 2026 15:59:53 +0000 by hackerkts