Singleton Pattern in Java: Thread-Safe Implementation Guide
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.
// ❌ 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(); } } }
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.
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.
// ✅ 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 } }
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.
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.
// ✅ 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()); } }
[AUDIT] Event #2 | File uploaded: report_q3.pdf | Logged by: main
Reflection attack blocked: IllegalArgumentException
Total events logged: 2
Instance hashCode: 1829164700
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.
// 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); } }
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
| Approach | Thread-Safe? | Lazy Init? | Reflection-Safe? | Serialization-Safe? | Best For |
|---|---|---|---|---|---|
| Naive (no sync) | ❌ No | ✅ Yes | ❌ No | ❌ No | Single-threaded demos only |
| Synchronized method | ✅ Yes | ✅ Yes | ❌ No | ❌ No | Simple cases, performance not critical |
| Double-Checked Locking | ✅ Yes (with volatile) | ✅ Yes | ❌ No | ❌ No | Most production multi-threaded code |
| Bill Pugh Holder | ✅ Yes | ✅ Yes | ❌ No | ❌ No | Clean, lazy init without volatile |
| Enum Singleton | ✅ Yes | ❌ No (eager) | ✅ Yes | ✅ Yes | Serializable or security-sensitive apps |
🎯 Key Takeaways
- A Singleton without
volatilein 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, novolatileneeded. - 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
volatilein Double-Checked Locking — Symptom: Rare, non-reproducible NullPointerExceptions or corrupt state in multi-threaded apps (happens maybe 1 in 10,000 runs). Root cause: withoutvolatile, the JVM can reorder memory writes and publish a non-null but half-initialized instance to other threads. Fix: always declare the instance field asprivate static volatile YourClass instance;— thatvolatilekeyword 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 areadResolve()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.
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.