DEV Community

Cover image for Understanding the Singleton Pattern: From Core Concept to Thread-Safe Java
Leo
Leo

Posted on Originally published at leostation.space

Understanding the Singleton Pattern: From Core Concept to Thread-Safe Java

Imagine you are building an application with a settings screen. A user switches the app theme to "dark", and ten different screens across the app need to apply that choice.

If each screen instantiated its own settings object with new, one screen might read "dark" while another still holds "light". What you actually need is one single settings object that is initialized once, exists as a single unique instance, and is shared across your entire application.

That is the exact problem the Singleton design pattern was created to solve.

Defined by the Gang of Four (GoF) in Design Patterns, its intent comes down to two guarantees:

  1. Ensure a class has only one instance.
  2. Provide a global point of access to it.

A plain global variable gives you the second guarantee, but it never stops someone from calling new and creating another instance of that class. A Singleton gives you both—and it does so using three core pieces:

  • A private constructor: locks the front door so ordinary external code cannot instantiate the class with new.
  • A static access method (getInstance()): acts as the single reception desk that everyone must go through.
  • One cached instance: stored in a private static field and returned on every subsequent call.

The Textbook Java Implementation (and Why It Breaks)

If you write the classic lazy initialization version in Java—where the instance is created only when someone asks for it the first time—it looks like this:

public class AppConfig {
    private static AppConfig instance;        // the one and only instance
    private String theme = "dark";

    private AppConfig() { }                   // nobody else can call new

    public static AppConfig getInstance() {   // the global access point
        if (instance == null) {
            instance = new AppConfig();       // created on first request
        }
        return instance;
    }

    public String getTheme() { return theme; }
}
Enter fullscreen mode Exit fullscreen mode

When you test this in a simple single-threaded main method, it works as expected:

AppConfig a = AppConfig.getInstance();
AppConfig b = AppConfig.getInstance();
System.out.println(a == b);   // true: both variables point to the same object
Enter fullscreen mode Exit fullscreen mode

The Hidden Trap: Concurrency

This simple implementation has a critical flaw: it is only safe when one task runs at a time.

In a multithreaded application, picture what happens when Thread A and Thread B call getInstance() at the exact same moment:

  1. Thread A checks if (instance == null), sees that instance is null, and starts instantiating AppConfig.
  2. Before Thread A finishes creating and assigning the object, Thread B also calls getInstance(), sees that instance is still null, and enters the if block too.
  3. Both threads call new AppConfig(), leaving each thread holding a different instance.

It is just like two employees checking a cabinet, seeing there is no key yet, and each going off to have a new one made: individually, each person acted sensibly, but together they broke the "only one unique key" rule.


Fixing It in Java (Without Slowing Everything Down)

Developers have come up with several ways to make a Singleton thread-safe in Java, and each one trades something for something else:

  1. Eager initialization: create the instance right when the JVM loads the class—simple, but wasteful if the object is never used.
  2. synchronized method: put synchronized on getInstance()—thread-safe, but slow because threads must wait in line on every call.
  3. Double-checked locking: synchronize only on the first creation, using the volatile keyword so writes to instance are visible across threads and the JVM doesn't reorder the initialization steps inside new.
  4. The Bill Pugh Holder idiom: place the instance inside a nested static Holder class that isn't initialized until getInstance() is called:
public class Logger {
    private Logger() { }

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

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

    public void log(String message) { System.out.println("LOG: " + message); }
}
Enter fullscreen mode Exit fullscreen mode

Because the Java Language Specification (JLS) guarantees that the class initialization phase is sequential, the Holder idiom gives you lazy initialization and thread safety with no if, no synchronized, and no volatile.

  1. The enum Singleton: recommended as the preferred approach in a summary of Joshua Bloch's Effective Java, because it also guards against two sneaky ways a private constructor can be bypassed (Reflection and Serialization).

The Part We Need to Talk About: The Architectural Price

Even when you write a rock-solid, thread-safe Singleton, convenience comes with a real cost.

The very feature that makes Singleton so tempting—shared global access—is the same reason it:

  • Violates the Single Responsibility Principle (the class manages both its business logic and its own lifecycle/uniqueness)
  • Creates tight coupling across your codebase
  • Can mask bad design
  • Makes unit testing difficult due to shared global state

So before reaching for a Singleton, the most important question isn't just "how do I make it thread-safe?"—it's:

Does this component truly have to exist as a single unique instance, or am I using it just because it's handy?


Read the Full Deep Dive

I just published the full, fully cited article over on Leo's Station, where I walk through all of these pieces step by step, including:

  • A visual breakdown of how Singleton works vs. global variables
  • Detailed walkthroughs and summary cheat-sheet of all 5 Java approaches (Eager, synchronized, Double-checked locking, Holder idiom, and Enum)
  • How Reflection and Serialization (readResolve()) can break a Singleton's uniqueness
  • Where the standard Java library itself uses Singletons (Runtime.getRuntime(), Desktop.getDesktop())
  • A clear guide on when to use Singleton (loggers, config, caches, thread/DB pools) and when to think twice

Read the full article here:

Understanding the Singleton Pattern: From Core Concept to Thread-Safe Java — Leo's Station


Let's discuss!

When you need a single shared component in Java today, do you still write a Singleton (like the Holder idiom or an enum), or do you rely entirely on Dependency Injection / IoC containers? Share your thoughts below!

Top comments (4)

Collapse
 
sgaggjhkjh profile image
Arjun Sharma •

The two threads checking for null at the same time is a clear way to show why lazy initialization breaks. For a shared settings object like the theme example, would you still prefer the Holder idiom over an enum when the instance needs lazy setup work?

Collapse
 
leo_station profile image
Leo •

Hi @sgaggjhkjh . Thank you for your commenting.
Yes, I'd still go with the Holder idiom for heavy lazy setup. Unlike an enum, which initializes the moment the class is referenced anywhere, the inner Holder class guarantees we only pay the setup cost when getInstance() is explicitly called, while also allowing class inheritance if needed.

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

thread-safe singleton, the trade-offs are the interesting part.

Collapse
 
leo_station profile image
Leo •

Yeah, in modern frameworks, the DI container handles this under the hood, so we rarely have to think about it. However, when I worked on large legacy projects where singletons were implemented manually, improper thread handling frequently caused subtle concurrency bugs.