Design Patterns

79. Singleton Pattern

The Singleton pattern ensures a class has exactly one instance; the enum singleton is the safest Java implementation โ€” it's thread-safe, serialization-safe, and immune to reflection attacks.

Java 21 (core language)

Explanation

Three modern implementations ranked best-to-worst:

1. Enum singleton (best): Java enums are inherently singletons. Serialisation is handled correctly (enum constants are preserved). Reflection cannot create new instances. Zero boilerplate. The only downside: cannot extend a class (can implement interfaces).

2. Initialization-on-demand holder idiom: a static nested class holds the instance; the class is only loaded when first accessed, providing lazy initialization and thread safety via the Java class-loading guarantee.

3. Double-checked locking: requires the volatile keyword (for visibility) and is verbose. Prefer the holder idiom instead.

Common pitfalls: eager initialization (instance created at class load even if never used), non-volatile double-checked locking (broken on pre-Java-5 memory models), failure to protect against reflection (use AccessibleObject) or serialization (implement readResolve()).

Example

import java.io.Serializable;

public class SingletonPatterns {
    // BEST: Enum singleton โ€” serialization-safe, reflection-safe, thread-safe
    enum AppConfig {
        INSTANCE;

        private String dbUrl = "jdbc:postgresql://localhost/mydb";

        public String getDbUrl() { return dbUrl; }
        public void setDbUrl(String url) { this.dbUrl = url; }

        public static AppConfig getInstance() { return INSTANCE; }
    }

    // GOOD: Initialization-on-demand holder (lazy, thread-safe)
    static class MetricsRegistry {
        private MetricsRegistry() {}

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

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

        public void record(String metric, double value) {
            System.out.printf("Metrics: %s = %.2f%n", metric, value);
        }
    }

    // AVOID: Double-checked locking (verbose, tricky to get right)
    static class LegacySingleton {
        private static volatile LegacySingleton instance; // volatile required!
        private LegacySingleton() {}
        public static LegacySingleton getInstance() {
            if (instance == null) {                     // first check (no lock)
                synchronized (LegacySingleton.class) {
                    if (instance == null) {             // second check (with lock)
                        instance = new LegacySingleton();
                    }
                }
            }
            return instance;
        }
    }

    public static void main(String[] args) {
        // Enum singleton
        AppConfig cfg = AppConfig.INSTANCE;
        cfg.setDbUrl("jdbc:postgresql://prod/mydb");
        System.out.println(AppConfig.getInstance().getDbUrl());
        System.out.println(cfg == AppConfig.INSTANCE); // true โ€” same instance

        // Holder idiom
        MetricsRegistry reg = MetricsRegistry.getInstance();
        reg.record("requests_per_sec", 1234.5);
        System.out.println(reg == MetricsRegistry.getInstance()); // true
    }
}

Sources