Singleton in Java, from the obvious version to the one that actually holds up

Published:

Singleton is probably one of the most talked-about and most frequently used design patterns. It is also one of the patterns that shows up in interviews again and again. Its purpose is simple: make sure a class has only one instance across the whole system.

That requirement is not arbitrary. Some things really are meant to exist only once: global configuration, a factory, a central controller, and similar objects that represent shared state or a single coordination point.

Of course, if you are leading a team, you could try to enforce this socially instead of technically. You could declare that a certain class must only ever have one global instance, and joke that anyone creating a second one gets fined. Inside a team, rules like that might work well enough. But once you are building a library or an API for others to use, those rules become meaningless. You cannot assume every caller will behave. That is why Singleton exists as a technical solution rather than just a convention.

To make the discussion concrete, it is easier to use Java here than C++, because Java exposes several important details around class loading, synchronization, and memory behavior.

The textbook Singleton

Let’s start with the familiar teaching example:

// version 1.0
public class Singleton {
    private static Singleton singleton = null;
    private Singleton() {  }
    public static Singleton getInstance() {
        if (singleton== null) {
            singleton= new Singleton();
        }
        return singleton;
    }
}

This version demonstrates the essential traits of a Singleton:

  1. The constructor is private, so code outside the class cannot instantiate it directly.
  2. Since normal instantiation is blocked, the class provides a static access point: getInstance().
  3. That method checks whether an instance already exists. If not, it creates one.
  4. The instance is stored in a private static field inside the class itself.
  5. Callers retrieve it through Singleton.getInstance().

If that feels like the whole story, it is not. This is only the classroom version.

The real problem begins with threads

Because a Singleton is global shared state, multithreading immediately makes things dangerous. In the code above, if several threads call getInstance() at the same time, they can all pass the singleton == null check before any one of them finishes construction. The result is multiple instances, and potentially memory leaks as well.

The natural reaction is: add synchronization.

// version 1.1
public class Singleton
{
    private static Singleton singleton = null;
    private Singleton() {  }
    public static Singleton getInstance() {
        if (singleton== null) {
            synchronized (Singleton.class) {
                singleton= new Singleton();
            }
        }
        return singleton;
    }
}

At first glance, this seems fine. But it still fails.

Why? Because several threads may still pass the outer if (singleton == null) check in parallel. Synchronization only makes them enter the critical section one at a time. Each of them can still execute new Singleton() in turn. So synchronization alone, placed only around construction, does not solve the problem.

That leads to the next version:

// version 1.2
public class Singleton
{
    private static Singleton singleton = null;
    private Singleton()  {  }
    public static Singleton getInstance()  {
        synchronized (Singleton.class) {
            if (singleton== null) {
        singleton= new Singleton();
            }
         }
        return singleton;
    }
}

This one does work in a multithreaded environment. Every thread is synchronized before checking and creating the instance, so only one object gets created.

But there is still a cost. The instance is only created once, yet every call to getInstance() pays the synchronization price forever, even after initialization is complete. That is wasteful. What should have been a one-time coordination cost now affects every later read.

Double-checked locking

To avoid synchronizing every call, you can first check outside the lock, and only synchronize if the instance has not been created yet:

// version 1.3
public class Singleton
{
    private static Singleton singleton = null;
    private Singleton()  {    }
    public static Singleton getInstance() {
        if (singleton== null)  {
            synchronized (Singleton.class) {
                if (singleton== null)  {
                    singleton= new Singleton();
                }
            }
        }
        return singleton;
    }
}

This is the classic double-check pattern.

Its logic is straightforward:

  1. The first check avoids synchronization after initialization is already done.
  2. If the instance is still missing, threads synchronize.
  3. The second check prevents a second thread from creating another object after the first one has already done so.

It looks like the ideal balance between correctness and performance. Unfortunately, there is still a subtle problem.

Why new Singleton() is not as simple as it looks

The issue is that this line:

singleton = new Singleton()

is not an atomic operation. Inside the JVM, it roughly consists of three steps:

  1. Allocate memory for the singleton object.
  2. Run the Singleton constructor to initialize the object.
  3. Assign the reference to singleton so it points to that memory.

The just-in-time compiler is allowed to reorder instructions for optimization, which means steps 2 and 3 may switch places. So the actual order could become either 1-2-3 or 1-3-2.

If 1-3-2 happens, then another thread may observe singleton as non-null before the constructor has actually finished. That second thread will return the object and try to use it, even though it has not been properly initialized yet. Errors follow naturally.

The usual fix is to make the field volatile:

// version 1.4
public class Singleton
{
    private volatile static Singleton singleton = null;
    private Singleton()  {    }
    public static Singleton getInstance()   {
        if (singleton== null)  {
            synchronized (Singleton.class) {
                if (singleton== null)  {
                    singleton= new Singleton();
                }
            }
        }
        return singleton;
    }
}

Using volatile does two important things here:

  1. It prevents each thread from keeping its own stale copy of the variable and instead makes reads come from main memory.
  2. It prevents instruction reordering around writes to that variable. In effect, a memory barrier is inserted after assignment, so reads cannot be moved ahead of it.

But there is an important caveat: this fix is reliable only in Java 1.5 and later. Earlier Java memory models had flaws that made this approach unsafe even with volatile.

A much simpler eager version

All of this can feel far too complicated for something that is supposed to be small and elegant. There is a much simpler form:

// version 1.5
public class Singleton
{
    private volatile static Singleton singleton = new Singleton();
    private Singleton()  {    }
    public static Singleton getInstance()   {
        return singleton;
    }
}

Here the instance is created when the class is loaded. Since the field is static and initialized during class loading, the JVM guarantees thread-safe construction.

The downside is just as clear: the object gets created whether anyone ever calls getInstance() or not.

That may not match what you want. Sometimes construction depends on other parts of the system having already completed their own setup, such as loading configuration or creating shared resources. In those cases, you want the object to be created exactly when getInstance() is first called, not earlier just because the class loader happened to load the class.

So eager initialization is simple, but it gives up control over creation timing.

Lazy initialization without synchronization overhead

A cleaner answer is the static holder pattern, which older editions of Effective Java recommended:

// version 1.6
public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

This version relies on the JVM’s class initialization mechanism to provide thread safety.

It has several advantages at once:

  • SingletonHolder is private, so nothing can touch it except getInstance().
  • The inner class is not loaded until getInstance() is actually called.
  • Initialization is thread-safe without explicit synchronization.
  • Later reads have no locking overhead.
  • It does not depend on a specific JDK workaround such as the post-1.5 volatile fix.

That makes it both lazy and efficient.

The most elegant form: enum

There is an even shorter approach:

public enum Singleton{
   INSTANCE;
}

Yes, an enum.

You access it with Singleton.INSTANCE, which is even more direct than calling getInstance().

Enum instance creation is thread-safe by default, so you do not need to worry about construction races. It also resists attacks that use reflection to invoke a private constructor. For everything else inside the enum, however, thread safety is still the programmer’s responsibility.

In practice, this version eliminates most of the usual Singleton problems while keeping the code extremely small. That is why newer editions of Effective Java favor it.

Singleton still has edge cases

Even if the implementation looks perfect, no code works correctly outside the assumptions it was designed for. Singleton is no exception. Several situations can still break the “only one instance” idea.

1. Multiple class loaders

This is a particularly Java-specific issue.

A JVM can have more than one ClassLoader, and each loader has its own namespace. A single ClassLoader will only load one Class object for a given type name, but different class loaders can each load what appears to be the same class independently.

Suppose ClassLoaderA loads class A and produces one runtime class object, while ClassLoaderB loads another separate runtime copy of A. Logically they represent the same class name, but to the JVM they are different types. If class A contains a static variable, each loader gets its own copy.

That means a Singleton can become “one per class loader” rather than “one per JVM.”

It may sound academic, but the problem is real. And it is not something the Singleton class itself can fully solve. The practical answer is architectural: make sure multiple class loaders do not each load the same Singleton when global uniqueness actually matters.

2. Serialization

If the Singleton is serializable—for example, if it represents application configuration—deserialization can create new instances unless you intervene.

Java provides a standard way to handle this through readResolve():

public class Singleton implements Serializable
{
    ......
    ......
    protected Object readResolve()
    {
        return getInstance();
    }
}

With this method, deserialization returns the canonical instance instead of producing a fresh object.

3. Multiple JVMs

If your system is spread across multiple JVM processes, the Singleton pattern can only guarantee one instance per JVM, not one instance globally across machines or processes.

That may happen in distributed setups such as EJB, RMI, or similar environments. In that world, “singleton” becomes a system design problem rather than something a class alone can enforce. Good architecture—or even operational rules—has to take over.

4. About volatile

volatile is sometimes described as a lighter form of synchronization. Compared with a synchronized block, it requires less code and usually less runtime overhead. But its power is also limited: it only solves part of what full synchronization can handle.

For Singleton, volatile helps with safe publication and instruction reordering, but it is not a universal replacement for synchronization. And as already noted, depending on it safely requires Java 1.5 or later.

5. Inheritance

Subclassing a Singleton can introduce multiple-instance problems. But in most implementations this is already blocked, because the constructor is private. Once that is true, inheritance is effectively off the table.

6. Code reuse

One last practical annoyance: many classes in a system may need Singleton behavior. Repeating the same pattern in every class becomes tedious and ugly.

In C++, abstracting the pattern can be easier because templates, friends, and stack allocation support this style naturally. A typical example looks like this:

template class Singleton
{
    public:
        static T& Instance()
        {
            static T theSingleInstance; //假设T有一个protected默认构造函数
            return theSingleInstance;
        }
};

class OnlyOne : public Singleton
{
    friend class Singleton;
    int example_data;

    public:
        int GetExampleData() const {return example_data;}
    protected:
        OnlyOne(): example_data(42) {}   // 默认构造函数
        OnlyOne(OnlyOne&) {}
};

int main( )
{
    cout << OnlyOne::Instance().GetExampleData() << endl;
    return 0;
}

Java does not offer the same tools in the same way, so making Singleton reusable there is more awkward. The pattern is simple at first glance, but the closer you look, the more language details and runtime behavior start to matter.

That is really the lesson here: Singleton is easy to write in a demo, but hard to get completely right once threading, initialization timing, reflection, serialization, class loading, and distributed deployment all enter the picture.