Singleton Design Pattern In Java Example

8 min read

The Singleton design pattern remains one of the most recognized and frequently debated patterns in software engineering. Now, at its core, it ensures that a class has only one instance while providing a global access point to that instance. For Java developers, mastering this pattern is not just about memorizing code snippets; it is about understanding the nuances of class loading, thread safety, and serialization within the Java Virtual Machine (JVM). Whether you are managing a database connection pool, a logging utility, or a configuration manager, the Singleton pattern offers a structured approach to controlling object lifecycle.

Understanding the Core Concept

Before diving into implementations, it is crucial to grasp why this pattern exists. In many applications, certain resources are expensive to create or represent a shared state that must remain consistent across the entire system. Creating multiple instances of a DatabaseConnection or a ConfigurationManager can lead to resource exhaustion, inconsistent data states, or difficult-to-debug concurrency issues.

The Singleton pattern solves this by restricting instantiation. Now, it violates the typical "new object" mentality by hiding the constructor and exposing a static method—traditionally named getInstance()—that acts as the gatekeeper. The first call creates the object; subsequent calls return the exact same reference Less friction, more output..

Still, Java’s multi-threaded nature and features like reflection and serialization introduce complexities that a naive implementation cannot handle. A solid Singleton must address:

  1. Lazy vs. Which means eager Initialization: Should the instance be created at class load time or on first request? In practice, 2. Day to day, Thread Safety: Can two threads simultaneously create two instances? In real terms, 3. Serialization/Deserialization: Does reading an object from a stream create a new instance? In real terms, 4. Which means Reflection Attacks: Can Constructor. setAccessible(true) break the singleton property?

Implementation Strategies: From Basic to Bulletproof

Let us explore the evolution of Singleton implementations in Java, highlighting the trade-offs of each approach.

1. Eager Initialization: Simplicity at a Cost

This is the most straightforward approach. The instance is created when the class is loaded into memory by the ClassLoader.

public class EagerSingleton {
    // Instance created at class loading time
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    // Private constructor prevents instantiation from outside
    private EagerSingleton() {}

    // Global access point
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

Pros: Thread-safe by default (ClassLoader guarantees initialization atomicity). Simple to write and understand. Cons: The instance is created even if the application never uses it. If initialization involves heavy I/O (like reading large config files), this slows down startup time unnecessarily. Exception handling in the static initializer is also cumbersome And it works..

2. Lazy Initialization (Non-Thread-Safe): The Naive Approach

This version creates the instance only when getInstance() is first called.

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton(); // Race condition here!
        }
        return instance;
    }
}

Critical Flaw: In a multi-threaded environment, two threads can pass the null check simultaneously before either assigns the instance, resulting in two distinct objects. Never use this in production Java applications.

3. Synchronized Method: Safe but Slow

Adding the synchronized keyword to the method fixes the race condition No workaround needed..

public static synchronized LazySingleton getInstance() {
    if (instance == null) {
        instance = new LazySingleton();
    }
    return instance;
}

Pros: Thread-safe. Cons: Performance bottleneck. Every single call to getInstance() acquires a lock, even after the instance is created. This synchronization overhead is unnecessary for 99.9% of the application's lifecycle Turns out it matters..

4. Double-Checked Locking (DCL): The Optimized Classic

This pattern attempts to reduce synchronization overhead by checking the instance twice: once without a lock, and once inside a synchronized block.

public class DclSingleton {
    // 'volatile' is MANDATORY here (Java 5+)
    private static volatile DclSingleton instance;

    private DclSingleton() {}

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

The volatile Keyword: This is the linchpin. Without volatile, the JVM might reorder the bytecode instructions for new DclSingleton() (allocate memory -> initialize object -> assign reference). A second thread could see a non-null reference pointing to a partially constructed object. volatile prevents this instruction reordering and ensures visibility across threads Simple as that..

Pros: High performance after initialization; thread-safe. Cons: Verbose, easy to get wrong (forgetting volatile), and still technically breakable via Reflection.

5. The Bill Pugh Solution: Initialization-on-Demand Holder Idiom

This is widely considered the best manual implementation for lazy initialization in Java versions prior to the Enum approach. It leverages the JVM's guarantee that a class is not loaded until it is referenced.

public class BillPughSingleton {
    private BillPughSingleton() {}

    // Static inner class - not loaded until getInstance() is called
    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

Why it works:

  1. Lazy: SingletonHelper is not loaded when BillPughSingleton loads. It loads only when getInstance() explicitly references SingletonHelper.INSTANCE.
  2. Thread-Safe: Class initialization is atomic and thread-safe by the JLS (Java Language Specification).
  3. No Synchronization Overhead: Zero locking cost on getInstance().
  4. Clean: Handles exceptions in initialization naturally.

6. Enum Singleton: The Modern Gold Standard

Since Java 5, Joshua Bloch (author of Effective Java) has championed the enum approach as the single best way to implement a Singleton. It handles serialization, reflection attacks, and thread safety automatically Most people skip this — try not to..

public enum EnumSingleton {
    INSTANCE;

    // You can add fields and methods here
    private String configData;

    public void doSomething() {
        System.out.println("Singleton action executed.");
    }

    public String getConfigData() {
        return configData;
    }

    public void setConfigData(String configData) {
        this.configData = configData;
    }
}

Usage:

EnumSingleton singleton = EnumSingleton.INSTANCE;
singleton.doSomething();

Why Enum Wins:

  • Serialization Safety: Java guarantees that enum constants are singletons within a JVM. The readObject mechanism is handled specially by the serialization runtime; it does not create new instances.
  • Reflection Proof: Constructor.setAccessible(true) throws an exception for enums. You literally cannot instantiate an enum via reflection.
  • Conciseness: Zero boilerplate code.
  • Thread Safety: Guaranteed by the language spec.

When not to use Enum: If your Singleton must extend a class (enums implicitly extend java.lang.Enum), or if you need lazy initialization that depends on complex runtime parameters not available at enum constant initialization time (though you can defer heavy lifting to a method call) Not complicated — just consistent..

Breaking the Pattern: Serialization and Reflection

Understanding how to break a Singleton is just as important as building one, as it informs your architectural decisions Worth keeping that in mind. Still holds up..

The Serialization Trap

If

The Serialization Trap

When a Singleton is serialized—converted to a byte stream and later reconstructed—the default deserialization process creates a new instance of the class, bypassing the Singleton contract. This is the classic “serialization attack” that can resurrect multiple copies of a supposedly single instance Worth keeping that in mind..

How it Happens

// Original class without protection
public class LazySingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

When LazySingleton is serialized and later deserialized, Java’s reflection‑based reconstruction invokes the private constructor, yielding a second instance that is indistinguishable from the original. The Singleton guarantee is broken.

Guarding Against It

1. Implement readObject with readResolve
The simplest, most widely‑adopted fix is to add a private Object readResolve() method that returns the canonical instance instead of allowing a new one to surface.

public class BillPughSingletonSerializable implements Serializable {
    private static final long serialVersionUID = 1L;

    // Existing Bill‑Pugh implementation …
    private static class SingletonHelper {
        private static final BillPughSingletonSerializable INSTANCE =
                new BillPughSingletonSerializable();
    }

    public static BillPughSingletonSerializable getInstance() {
        return SingletonHelper.INSTANCE;
    }

    // Prevent a new instance during deserialization
    private Object readResolve() {
        return getInstance();
    }
}

Because readResolve is invoked instead of the default constructor when an object is being reconstructed, the deserialization process is forced to return the existing singleton.

2. Make the Class final
Declaring the Singleton class as final prevents malicious subclasses from overriding readObject or writeObject methods and re‑introducing extra instances Worth knowing..

3. Use the Enum Approach (the Ultimate Shield)
Enums are inherently immune to this problem. The JVM guarantees that each enum constant is a singleton, and the serialization runtime treats them specially—readObject on an enum simply returns the existing constant. For most modern codebases, the enum pattern is the cleanest way to achieve serialization safety without any extra boilerplate The details matter here..

Reflection Attacks

Even with a private constructor, a determined attacker can use reflection to bypass access checks:

Constructor ctor =
    BillPughSingleton.class.getDeclaredConstructor();
ctor.setAccessible(true);
BillPughSingleton rogue = ctor.newInstance(); // Could create a second instance!

Defensive Measures

  • Throw an exception in the private constructor if the class has already been instantiated. This pattern is simple but requires an extra flag and careful synchronization.
  • Use an enum—as already discussed, reflection cannot instantiate an enum constant.
  • Implement a “singleton guard” that checks a static flag and throws IllegalStateException when a second instance is attempted.
public final class ReflectiveSafeSingleton {
    private static boolean instantiated = false;

    private ReflectiveSafeSingleton() {
        synchronized (ReflectiveSafeSingleton.class) {
            if (instantiated) {
                throw new IllegalStateException(
                    "Cannot instantiate a Singleton – already created.");
            }
            instantiated = true;
        }
    }

    private static final class Holder {
        private static final ReflectiveSafeSingleton INSTANCE =
                new ReflectiveSafeSingleton();
    }

    public static ReflectiveSafeSingleton getInstance() {
        return Holder.INSTANCE;
    }
}

Because the check occurs inside the constructor and is synchronized, even a reflective call will be blocked after the first instantiation Small thing, real impact. Surprisingly effective..

Putting It All Together: A strong Singleton Blueprint

Below is a compact, production‑ready template that addresses serialization, reflection, lazy initialization, and thread safety in a single, maintainable class.

public final class RobustSingleton implements Serializable {

    private static final long serialVersionUID = 20240815L;
    private static final class Holder {
        private static final RobustSingleton INSTANCE = new RobustSingleton();
    }

    private RobustSingleton() {
        // Guard against reflective instantiation
        if (Holder.Practically speaking, iNSTANCE ! = null) {
            throw new IllegalStateException(
                "Singleton already instantiated – use getInstance().

    public static RobustSingleton getInstance() {
        return Holder.INSTANCE;
    }

    // Ensure a singleton is returned during deserialization
    private Object readResolve() {
        return getInstance();
    }
}
  • **Lazy‑loading
New This Week

Brand New

These Connect Well

Cut from the Same Cloth

Thank you for reading about Singleton Design Pattern In Java Example. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home