Singleton Pattern Implementation Strategies in Java

The singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to it. While conceptually simple, its correct implementation—especially under concurrent conditions—requires careful attention to initialization timing, thread safety, memory visibility, and JVM-level optimizations.

Hungry Initialization (Eager Instantiation)

This approach creates the singleton instance at class-loading time. It’s inherently thread-safe because static field initialization is guarded by the JVM’s class initialization lock.

1. Public Static Field

public class EagerSingleton {
    private EagerSingleton() {}
    public static final EagerSingleton INSTANCE = new EagerSingleton();
}

2. Private Static Field with Accessor

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

3. Lazy-Loaded via Static Inner Class

This variant defers instantiation until the first call to getInstance(), leveraging the JVM’s guarantee that static inner classes are initialized only upon first active use. It combines thread safety, lazy initialization, and zero synchronization overhead.

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

Note: This technique does not prevent deserialization from producing duplicate instances unless readResolve() is implemented.

Lazy Initialization (On-Demand Instantiation)

These implementations delay object creation until the first request. They require explicit concurrency control to avoid race conditions.

Basic (Not Thread-Safe)

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

Synchronized Method (Thread-Safe but Infeficient)

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

Double-Checked Locking (DCL) with volatile

Without volatile, DCL is unsafe due to instruction reordering: the JVM may assign a partially constructed object reference before its constructor completes. The volatile keyword prevents such reordering and ensures visibility across threads.

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

Enum-Based Singleton

Declaring a singleton as an enum is the most robust approach in Java. The JVM guarantees that enum constants are instantiated exactly once, even during serialization, reflection, or deserialization attacks. No additional safeguards are needed.

public enum EnumSingleton {
    INSTANCE;
    public void performAction() {
        // business logic
    }
}

Key Considerations

  • volatile ensures visibility and prevents instruction reordering—but does not provide atomicity.
  • Static inner class and enum approaches are preferred for correctness and simplicity.
  • Basic lazy initialization without synchronization fails under concurrency.
  • DCL requires volatile on the instance field; omitting it reintroduces subtle race conditions.
  • All non-enum variants must implement readResolve() to defend against serialization-based instantiation.

Tags: java singleton-pattern thread-safety volatile JVM

Posted on Wed, 30 Sep 2026 16:17:02 +0000 by faifas