Home Java Singleton Pattern in Java: Thread-Safe Implementation Guide

Singleton Pattern in Java: Thread-Safe Implementation Guide

In Plain English 🔥
Imagine your school has one printer in the library. No matter which classroom you walk from, you all use that same printer — nobody brings a second one in. The Singleton pattern works exactly like that: it guarantees that a class has only ONE instance in your entire program, and every part of the code that asks for it gets handed the exact same object. That's it. One object, shared everywhere, created once.
⚡ Quick Answer
Imagine your school has one printer in the library. No matter which classroom you walk from, you all use that same printer — nobody brings a second one in. The Singleton pattern works exactly like that: it guarantees that a class has only ONE instance in your entire program, and every part of the code that asks for it gets handed the exact same object. That's it. One object, shared everywhere, created once.

Every non-trivial Java application has resources that are expensive to create, dangerous to duplicate, or logically absurd to have more than one of — a connection pool, an application-wide config loader, a logging service. If every class that needs the logger creates its own instance, you waste memory, fragment your log output, and risk race conditions. This is the exact problem the Singleton pattern was designed to solve, and it's why you'll find it baked into frameworks like Spring, Hibernate, and the Java standard library itself.

Why the Naive Singleton Breaks Under Multithreading

The most obvious Singleton implementation — a private constructor plus a static getInstance() method — works perfectly in single-threaded code. But the moment two threads call getInstance() simultaneously before the instance is created, both can slip past the null check, and you end up with two separate instances. That defeats the entire purpose.

This is called a race condition, and it's subtle. In production it might only surface under load, making it one of those nasty bugs that's hard to reproduce locally. The code below shows the broken version first so you can see exactly what goes wrong — then we fix it.

The broken version has no synchronization on the critical section where the instance is first created. Thread A reads instance == null as true, gets paused by the scheduler, Thread B also reads instance == null as true and creates the object, then Thread A resumes and creates a SECOND object. Two singletons. Chaos.

BrokenSingleton.java · JAVA
1234567891011121314151617181920212223242526272829303132
// ❌ BROKEN: This singleton is NOT thread-safe.
// Two threads can both pass the null check before either sets the field.

public class BrokenSingleton {

    // The one instance — starts as null until first requested
    private static BrokenSingleton instance;

    // Private constructor prevents anyone outside this class calling `new BrokenSingleton()`
    private BrokenSingleton() {
        System.out.println("BrokenSingleton created by thread: " + Thread.currentThread().getName());
    }

    // ❌ No synchronization — race condition lives here
    public static BrokenSingleton getInstance() {
        if (instance == null) {               // Thread A and Thread B can BOTH pass this check
            instance = new BrokenSingleton(); // ...and BOTH create an instance
        }
        return instance;
    }

    public static void main(String[] args) throws InterruptedException {
        // Spin up 5 threads all trying to grab the instance at the same time
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                BrokenSingleton singleton = BrokenSingleton.getInstance();
                System.out.println("Got instance with hashCode: " + singleton.hashCode());
            });
            thread.start();
        }
    }
}
▶ Output
BrokenSingleton created by thread: Thread-0
BrokenSingleton created by thread: Thread-2
Got instance with hashCode: 1829164700
Got instance with hashCode: 2018699554
Got instance with hashCode: 1829164700
Got instance with hashCode: 1829164700
Got instance with hashCode: 2018699554

// Notice: TWO different hashCodes — two separate instances were created. Singleton is broken.
⚠️
Watch Out:The broken singleton will often work fine in local testing because your dev machine processes it fast enough that threads rarely collide. It only blows up under real load. Never ship an unsynchronized singleton in multi-threaded code — not even 'temporarily'.

Double-Checked Locking: The Production-Ready Singleton

The gold-standard fix is called Double-Checked Locking (DCL). The idea: only synchronize during the brief window when the instance is first being created. Once it exists, reads are unsynchronized and fast.

The trick that makes it work is the volatile keyword on the instance field. Without volatile, the JVM's memory model allows instruction reordering — the JVM could write a half-constructed object to instance before its constructor finishes, and another thread could see a non-null but broken object. volatile forces a full memory barrier, preventing that reordering.

DCL is the approach you'll see in professional codebases. It's performant because synchronization only happens once (during creation), and it's correct because volatile closes the memory-visibility gap. It's also worth knowing the Bill Pugh / Initialization-on-Demand Holder idiom as an elegant alternative — shown at the end of this section.

ThreadSafeSingleton.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
// ✅ CORRECT: Double-Checked Locking Singleton — thread-safe and performant
// Real-world use case: Application-wide configuration manager

public class ThreadSafeSingleton {

    // `volatile` is NON-NEGOTIABLE here — it prevents the JVM from publishing
    // a half-constructed object to other threads due to instruction reordering
    private static volatile ThreadSafeSingleton instance;

    private final String configFilePath;
    private final int maxConnections;

    // Private constructor loads config once — expensive operation done only once
    private ThreadSafeSingleton() {
        System.out.println("[CONFIG] Loading configuration... (thread: "
                + Thread.currentThread().getName() + ")");
        this.configFilePath = "/etc/myapp/config.yaml"; // simulate reading from disk
        this.maxConnections = 50;
    }

    public static ThreadSafeSingleton getInstance() {
        // FIRST check (no lock): if instance already exists, return immediately — fast path
        if (instance == null) {

            // Only one thread can enter this block at a time
            synchronized (ThreadSafeSingleton.class) {

                // SECOND check (inside lock): another thread may have created
                // the instance while we were waiting to acquire the lock
                if (instance == null) {
                    instance = new ThreadSafeSingleton();
                }
            }
        }
        return instance; // returns the same object every single time after creation
    }

    public String getConfigFilePath() { return configFilePath; }
    public int getMaxConnections()    { return maxConnections; }

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            ThreadSafeSingleton config = ThreadSafeSingleton.getInstance();
            System.out.println(Thread.currentThread().getName()
                    + " -> hashCode: " + config.hashCode()
                    + ", maxConnections: " + config.getMaxConnections());
        };

        // Launch 6 threads simultaneously — only ONE constructor call should happen
        Thread[] threads = new Thread[6];
        for (int i = 0; i < 6; i++) {
            threads[i] = new Thread(task, "Worker-" + i);
        }
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join(); // wait for all threads to finish
    }
}
▶ Output
[CONFIG] Loading configuration... (thread: Worker-0)
Worker-0 -> hashCode: 1829164700, maxConnections: 50
Worker-1 -> hashCode: 1829164700, maxConnections: 50
Worker-2 -> hashCode: 1829164700, maxConnections: 50
Worker-3 -> hashCode: 1829164700, maxConnections: 50
Worker-4 -> hashCode: 1829164700, maxConnections: 50
Worker-5 -> hashCode: 1829164700, maxConnections: 50

// One constructor call. Six threads. Same hashCode every time. That's a correct Singleton.
🔥
Pro Tip:If you don't need lazy initialization (i.e., it's fine to create the instance when the class loads), skip DCL entirely and use the Bill Pugh Holder idiom: a private static inner class that holds the instance. The JVM's class-loading mechanism guarantees thread safety for free, with zero synchronization code. It's simpler, safer, and just as performant.

Enum Singleton: The Bulletproof Version You Should Know

Here's a trick that catches a lot of developers off guard: Java's enum type is itself a Singleton. The JVM guarantees that each enum constant is instantiated exactly once, is thread-safe by default, and — crucially — is protected against two attacks that break every other Singleton implementation: serialization and reflection.

With a normal Singleton, a malicious or careless developer can call Constructor.setAccessible(true) via reflection and invoke the private constructor, creating a second instance. Similarly, deserializing a serialized Singleton creates a fresh object. Both of these bypass your getInstance() logic entirely.

The enum approach blocks both attacks. The JVM outright refuses to instantiate enum types via reflection, and the serialization mechanism for enums returns the existing constant rather than a new instance. Joshua Bloch — the author of Effective Java — explicitly recommends this approach. Use it when you're building a Singleton that might be serialized or when security matters.

EnumSingleton.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243
// ✅ BULLETPROOF: Enum Singleton — reflection-safe, serialization-safe, thread-safe
// Real-world use case: Application event bus or centralized audit logger

public enum EnumSingleton {

    // This is the single instance — the JVM creates it exactly once, guaranteed
    INSTANCE;

    private int eventCount = 0;
    private final String logPrefix = "[AUDIT]";

    // Your actual business methods go here — treat INSTANCE like a regular object
    public void logEvent(String eventDescription) {
        eventCount++;
        System.out.println(logPrefix + " Event #" + eventCount
                + " | " + eventDescription
                + " | Logged by: " + Thread.currentThread().getName());
    }

    public int getTotalEventCount() {
        return eventCount;
    }

    public static void main(String[] args) throws Exception {
        // Normal usage — call via INSTANCE
        EnumSingleton.INSTANCE.logEvent("User login: alice@example.com");
        EnumSingleton.INSTANCE.logEvent("File uploaded: report_q3.pdf");

        // Try to break it with reflection — the JVM will throw an exception
        try {
            java.lang.reflect.Constructor<EnumSingleton> constructor =
                    EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
            constructor.setAccessible(true);
            EnumSingleton hackedInstance = constructor.newInstance("FAKE", 0); // ❌ This will fail
        } catch (Exception e) {
            // JVM protects enum — reflection attack is blocked
            System.out.println("Reflection attack blocked: " + e.getClass().getSimpleName());
        }

        System.out.println("Total events logged: " + EnumSingleton.INSTANCE.getTotalEventCount());
        System.out.println("Instance hashCode: " + EnumSingleton.INSTANCE.hashCode());
    }
}
▶ Output
[AUDIT] Event #1 | User login: alice@example.com | Logged by: main
[AUDIT] Event #2 | File uploaded: report_q3.pdf | Logged by: main
Reflection attack blocked: IllegalArgumentException
Total events logged: 2
Instance hashCode: 1829164700
⚠️
Interview Gold:When an interviewer asks 'What's the best way to implement a Singleton in Java?', saying 'Enum Singleton, per Joshua Bloch's Effective Java Item 3' immediately signals you know the language deeply. Most candidates only know DCL. This answer will make you stand out.

Real-World Singletons in Java — Where You're Already Using Them

The Singleton pattern isn't just an academic exercise — you use it constantly without realizing it. Understanding where it already appears in Java helps you recognize when to reach for it in your own code.

Runtime.getRuntime() returns the single JVM Runtime instance. System.out is a static final field — one PrintStream, used everywhere. Spring's ApplicationContext is effectively a Singleton container — every bean marked @Scope("singleton") (the default) is managed as a Singleton by the framework. Hibernate's SessionFactory is designed to be instantiated once per application because creating it is extremely expensive.

The pattern to recognize is this: if creating multiple instances of something would either waste significant resources or produce logically incorrect behavior (imagine two separate config stores with different values), that thing is a Singleton candidate.

When NOT to use it: Singletons make unit testing harder because they carry state across tests. They're also a form of global state, which creates hidden coupling between classes. Use dependency injection to pass the single instance around rather than having classes call getInstance() directly — that way you can swap in a mock during testing.

RealWorldSingletonDemo.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// Demonstrating real Singletons already in the Java standard library

public class RealWorldSingletonDemo {

    public static void main(String[] args) {

        // --- 1. Runtime Singleton ---
        // Runtime.getRuntime() always returns the same Runtime object
        Runtime jvmRuntime = Runtime.getRuntime();
        System.out.println("Available processors: " + jvmRuntime.availableProcessors());
        System.out.println("Max JVM memory (bytes): " + jvmRuntime.maxMemory());

        // Calling it again returns the SAME instance — same hashCode
        Runtime anotherReference = Runtime.getRuntime();
        System.out.println("Same Runtime instance? " + (jvmRuntime == anotherReference)); // true

        // --- 2. Bill Pugh Holder Idiom (elegant lazy singleton, no synchronization needed) ---
        // The inner class is only loaded when getInstance() is first called
        DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
        DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance();
        System.out.println("Same pool instance? " + (pool1 == pool2)); // true
        pool1.executeQuery("SELECT * FROM users WHERE active = true");
    }
}

// Bill Pugh Initialization-on-Demand Holder — clean, lazy, thread-safe without volatile or synchronized
class DatabaseConnectionPool {

    private final int poolSize;

    private DatabaseConnectionPool() {
        this.poolSize = 10;
        System.out.println("[DB POOL] Connection pool initialized with " + poolSize + " connections.");
    }

    // The JVM only loads this inner class when getInstance() is first called
    // Class loading in Java is thread-safe — no extra synchronization needed
    private static final class PoolHolder {
        private static final DatabaseConnectionPool POOL_INSTANCE = new DatabaseConnectionPool();
    }

    public static DatabaseConnectionPool getInstance() {
        return PoolHolder.POOL_INSTANCE; // inner class loaded here on first call
    }

    public void executeQuery(String sql) {
        System.out.println("[DB POOL] Executing on pool (size=" + poolSize + "): " + sql);
    }
}
▶ Output
Available processors: 8
Max JVM memory (bytes): 4294967296
Same Runtime instance? true
[DB POOL] Connection pool initialized with 10 connections.
Same pool instance? true
[DB POOL] Executing on pool (size=10): SELECT * FROM users WHERE active = true
🔥
The DI Rule:In Spring or any DI framework, let the container manage your Singleton lifecycle. Don't manually implement getInstance() in a Spring @Component — Spring already guarantees one instance per ApplicationContext. Hand-rolling a Singleton inside a framework that manages them for you is a code smell.
ApproachThread-Safe?Lazy Init?Reflection-Safe?Serialization-Safe?Best For
Naive (no sync)❌ No✅ Yes❌ No❌ NoSingle-threaded demos only
Synchronized method✅ Yes✅ Yes❌ No❌ NoSimple cases, performance not critical
Double-Checked Locking✅ Yes (with volatile)✅ Yes❌ No❌ NoMost production multi-threaded code
Bill Pugh Holder✅ Yes✅ Yes❌ No❌ NoClean, lazy init without volatile
Enum Singleton✅ Yes❌ No (eager)✅ Yes✅ YesSerializable or security-sensitive apps

🎯 Key Takeaways

  • A Singleton without volatile in a multi-threaded Java app is a ticking time bomb — the bug only appears under load and is nearly impossible to reproduce in dev.
  • The Enum Singleton is the only implementation that's automatically safe against both reflection and serialization attacks — it's Joshua Bloch's explicit recommendation in Effective Java.
  • The Bill Pugh Holder idiom gives you lazy initialization and thread safety for free by exploiting the JVM's own class-loading guarantee — no synchronized, no volatile needed.
  • Singletons are global state in disguise — always inject them rather than calling getInstance() inside business logic, or your code becomes untestable.

⚠ Common Mistakes to Avoid

  • Mistake 1: Forgetting volatile in Double-Checked Locking — Symptom: Rare, non-reproducible NullPointerExceptions or corrupt state in multi-threaded apps (happens maybe 1 in 10,000 runs). Root cause: without volatile, the JVM can reorder memory writes and publish a non-null but half-initialized instance to other threads. Fix: always declare the instance field as private static volatile YourClass instance; — that volatile keyword is not optional, it's load-bearing.
  • Mistake 2: Breaking the Singleton with Java Serialization — Symptom: After serializing and deserializing your Singleton, deserializedInstance == SingletonClass.getInstance() returns false — you now have two instances. Fix: either use the Enum approach (which handles this automatically), or add a readResolve() method to your class: protected Object readResolve() { return getInstance(); }. This tells the serialization mechanism to return the existing instance instead of the newly deserialized one.
  • Mistake 3: Calling getInstance() directly inside other classes (tight coupling) — Symptom: Unit tests for classes that use the Singleton become nearly impossible to isolate because the real Singleton always initializes (maybe hitting a database, reading a file). Fix: accept the Singleton through the constructor or a setter (dependency injection). In tests, pass a mock or stub. In production, pass the real instance. Your classes shouldn't care whether it's a Singleton — that's the Singleton's business, not the caller's.

Interview Questions on This Topic

  • QWhy does Double-Checked Locking require the `volatile` keyword in Java, and what specific problem does omitting it cause?
  • QHow would you break a classic (non-enum) Singleton implementation, and how do you protect against each attack vector?
  • QWhat are the downsides of the Singleton pattern in terms of testability and application design, and how does dependency injection mitigate them?

Frequently Asked Questions

Is the Singleton pattern the same as a static class in Java?

No, they're different. A static class (a class with only static methods) can't implement interfaces, can't be passed as an object, and can't be lazily initialized. A Singleton is a real object — it can implement interfaces, be injected as a dependency, and be swapped out for a mock in tests. Prefer Singleton over a purely static class whenever you need polymorphism or testability.

Does Spring's @Component annotation automatically make a bean a Singleton?

Yes. Spring beans are Singleton-scoped by default, meaning the ApplicationContext creates exactly one instance and returns that same instance every time it's injected. You don't need to implement the Singleton pattern manually inside a Spring application — just let the container manage it. Only implement it yourself in pure Java (non-Spring) code.

Can I use the Singleton pattern in a distributed system across multiple JVMs?

No — the classic Java Singleton only guarantees a single instance within one JVM process. If your app runs on multiple servers or in multiple containers (as microservices do), each JVM has its own Singleton instance. For truly global shared state across a distributed system, you need an external store like Redis, a distributed cache, or a shared database — not the Singleton pattern.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousDesign Patterns in JavaNext →Factory Pattern in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged