Fundamentals 14 min read

Understanding the Singleton Design Pattern in Java: Lazy, Eager, Double‑Check, Volatile, Static Inner Class and Enum Implementations

This article explains the Singleton design pattern in Java, covering its definition, the differences between lazy and eager implementations, thread‑safety issues, multiple code versions (simple, synchronized, double‑checked locking, volatile), and alternative approaches using static inner classes and enums, while discussing atomic operations and instruction reordering.

Java Captain
Java Captain
Java Captain
Understanding the Singleton Design Pattern in Java: Lazy, Eager, Double‑Check, Volatile, Static Inner Class and Enum Implementations

The Singleton pattern ensures that a class has only one instance, a concept familiar to most developers but often misunderstood when concurrency is involved.

According to Wikipedia, a Singleton object’s class must guarantee a single instance, which can be verified by checking that only one object is ever created.

Singleton implementations fall into two major categories: lazy (instance created on first use) and eager (instance created when the class is loaded).

Lazy implementations :

Simple version (Version 1):

public class Single1 {
    private static Single1 instance;
    public static Single1 getInstance() {
        if (instance == null) {
            instance = new Single1();
        }
        return instance;
    }
}

Making the constructor private prevents external instantiation.

public class Single1 {
    private static Single1 instance;
    private Single1() {}
    public static Single1 getInstance() {
        if (instance == null) {
            instance = new Single1();
        }
        return instance;
    }
}

In multithreaded environments, two threads may both see instance == null and create separate objects, breaking the Singleton guarantee.

Synchronized version (Version 2):

public class Single2 {
    private static Single2 instance;
    private Single2() {}
    public static synchronized Single2 getInstance() {
        if (instance == null) {
            instance = new Single2();
        }
        return instance;
    }
}

Adding synchronized ensures only one thread can execute the method at a time, but it incurs a performance penalty because every call acquires the lock.

Double‑checked locking version (Version 3):

public class Single3 {
    private static Single3 instance;
    private Single3() {}
    public static Single3 getInstance() {
        if (instance == null) {
            synchronized (Single3.class) {
                if (instance == null) {
                    instance = new Single3();
                }
            }
        }
        return instance;
    }
}

This reduces locking overhead by checking instance == null twice, but it can still fail due to instruction reordering and non‑atomic object creation.

Volatile version (Version 4):

public class Single4 {
    private static volatile Single4 instance;
    private Single4() {}
    public static Single4 getInstance() {
        if (instance == null) {
            synchronized (Single4.class) {
                if (instance == null) {
                    instance = new Single4();
                }
            }
        }
        return instance;
    }
}

Declaring instance as volatile prevents instruction reordering and ensures that a write to the variable happens-before any subsequent reads, eliminating the subtle race condition of Version 3.

Key concepts :

Atomic operation: an indivisible action that cannot be interrupted by thread scheduling (e.g., a simple assignment like m = 6 ).

Instruction reordering: the JVM may reorder independent statements to improve performance, which can expose non‑atomic sequences to race conditions.

Eager implementation :

public class SingleB {
    private static final SingleB INSTANCE = new SingleB();
    private SingleB() {}
    public static SingleB getInstance() {
        return INSTANCE;
    }
}

This approach relies on class loading synchronization, avoiding most multithreading issues, but it creates the instance at load time, which may waste resources or cause initialization order problems.

Alternative approaches :

Static inner‑class (Effective Java 1):

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

The inner class is loaded only when getInstance() is first called, combining lazy loading with the JVM’s class‑loading guarantees.

Enum singleton (Effective Java 2):

public enum SingleInstance {
    INSTANCE;
    public void fun1() {
        // do something
    }
}

Enum instances are inherently thread‑safe, provide serialization for free, and prevent reflection attacks, making them a concise and robust Singleton solution, though they cannot be extended.

In summary, while the basic Singleton pattern appears simple, real‑world usage demands careful consideration of thread safety, initialization timing, and language‑specific features; no single implementation is universally perfect.

JavaenumThread Safetydesign patternvolatileSingletonLazy Initialization
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.