Understanding the Singleton Design Pattern in Java

Overview

The Singleton pattern ensures that a class has only one instance throughout an application while providing a global access point to that instance. This is particularly useful for resource-intensive objects that should be shared, such as database connection pools, configuration managers, or logging utilities.

For example, SessionFactory in Hibernate manages session creation. Since creating a SessionFactory is expensive and an application typically needs only one instance, the Singleton pattern is the ideal choice.

Implementation Variants

Eager Initialization (Static Field)

public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1); // true
        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());
    }
}

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

Pros: Simple implementation with no thread safety concerns.

Cons: Instance is created at class loading time, potentially wasting memory if never used. This approach does not support lazy loading.

Eager Initialization (Static Block)

public class SingletonTest02 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1); // true
    }
}

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

Pros: Same benefits as the static field variant.

Cons: Same drawbacks—lacks lazy loading and may consume memory unnecessarily.

Lazy Initialization (Not Thread-Safe)

class Singleton {
    private static Singleton INSTANCE;
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

Pros: Achieves lazy loading.

Cons: Unsafe in multithreaded environments. Multiple threads can pass the null check simultaneously and create separate instances.

Synchronized Method

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

Pros: Thread-safe.

Cons: Performance bottleneck. Every thread must acquire the monitor to check INSTANCE, even after initialization. Only the first call requires synchronization.

Synchronized Block (Incorrect)

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

Pros: None in this form.

Cons: Still not thread-safe. A thread may pass the outer null check before another thread enters the synchronized block, resulting in multiple instances.

Double-Checked Locking

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

Pros: Thread-safe, lazy-loading capable, and efficient. The outer check avoids synchronization after initialization, while the inner check prevents multiple instance creation.

Cons: Requires Java 5 or higher for correct volatile semantics.

Bill Pugh Singleton (Static Inner Class)

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

Pros: Thread-safe without synchronization. The JVM guarantees that the inner class loads only when getInstance() is called, and class initialization is inherently thread-safe.

Cons: None significant.

Enum Singleton

enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        System.out.println("Operation performed");
    }
}

Pros: Extremely simple, inherently thread-safe, and protected against reflection and deserialization attacks.

Cons: Not suitable when the singleton must extend another class (enums cannot inherit other classes).

JDK Internals

The java.lang.Runtime class uses the eager initialization pattern:

public class Runtime {
    private static final Runtime INSTANCE = new Runtime();
    
    public static Runtime getRuntime() {
        return INSTANCE;
    }
}

Since the JVM always creates a Runtime instance, eager initialization is appropriate here.

When to Use

Apply the Singleton pattern when:

  • Creating an object is expensive (database connections, file handles)
  • Exactly one shared instance is required across the application
  • A global access point is needed (configuration, logging)

Summary

Variant Thread-Safe Lazy Load Performence Recommended
Eager (static field/block) Yes No Excellent When instance is always needed
Lazy (unsafe) No Yes Excellent Never in production
Synchronized method Yes Yes Poor Rarely
Synchronized block (broken) No Yes Good Never
Double-checked locking Yes Yes Good Yes
Static inner class Yes Yes Excellent Yes
Enum Yes Yes Excellent Yes

Tags: design-patterns singleton java creational-patterns

Posted on Tue, 01 Sep 2026 16:08:28 +0000 by erfg1