Is Your Java Singleton Really a Singleton? Thread, Reflection & Serialization Pitfalls

This article examines various Java singleton implementations—eager, lazy, double‑checked locking, static inner class, enum, and Kotlin object—analyzing their thread safety, reflection and serialization vulnerabilities, and shows how to harden them using volatile, constructor guards, and readResolve.

Java Backend Technology
Java Backend Technology
Java Backend Technology
Is Your Java Singleton Really a Singleton? Thread, Reflection & Serialization Pitfalls

1. General Singleton Implementations

1.1 Eager Singleton

public class HungrySingleton {
    private static final HungrySingleton mInstance = new HungrySingleton();
    private HungrySingleton() {}
    public static HungrySingleton getInstance() { return mInstance; }
}

The private constructor prevents external instantiation. The eager version creates the instance at class‑loading time, which is thread‑safe, but it may waste resources if the instance is heavy.

1.2 Lazy Singleton

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

Instance creation is delayed until getInstance() is called, but the synchronized method incurs a performance cost.

1.3 Double‑Checked Locking (DCL)

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

DCL reduces synchronization overhead but suffers from possible instruction reordering, which can expose a partially constructed object to other threads.

Reordering steps in JVM: Allocate memory for the object. Assign the reference to mInstance . Initialize the object.

When reordering occurs, Thread A may assign the reference before the object is fully initialized, allowing Thread B to see a non‑initialized instance.

1.4 Fixing DCL with volatile

Marking mInstance as volatile prevents the reordering that breaks DCL, making it truly thread‑safe in JDK 1.5 and later.

1.5 Static Inner Class Singleton

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

This approach combines lazy loading with class‑loader‑based thread safety, avoiding explicit synchronization.

2. Is It Really a Singleton?

Beyond thread safety, a robust singleton must also resist reflection and serialization attacks.

2.1 Reflection Safety

public static void main(String[] args) throws Exception {
    DCLSingleton s1 = DCLSingleton.getInstance();
    Constructor<DCLSingleton> ctor = DCLSingleton.class.getDeclaredConstructor();
    ctor.setAccessible(true);
    DCLSingleton s2 = ctor.newInstance();
    System.out.println(s1.hashCode());
    System.out.println(s2.hashCode());
}

The output shows two different hash codes, proving that reflection can break the singleton. To prevent this, the constructor can be guarded:

private DCLSingleton() {
    if (mInstance != null) {
        throw new RuntimeException("Reflection is not allowed!");
    }
}

2.2 Serialization Safety

public static void main(String[] args) throws Exception {
    DCLSingleton s1 = DCLSingleton.getInstance();
    // Serialize
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
    out.writeObject(s1);
    out.close();
    // Deserialize
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
    DCLSingleton s2 = (DCLSingleton) in.readObject();
    System.out.println(s1.hashCode());
    System.out.println(s2.hashCode());
}

Deserialization creates a new instance. Overriding readResolve() fixes this:

protected Object readResolve() { return getInstance(); }

2.3 Enum Singleton

public enum EnumSingleton { INSTANCE; }

Enum singletons are inherently thread‑safe, serialization‑safe, and resistant to reflection because the JVM forbids reflective creation of enum objects.

2.4 Kotlin Object Singleton

object KotlinSingleton { }

Kotlin’s object is compiled to a class with a static final instance, effectively an eager singleton.

3. Takeaways

While many singleton implementations exist, the enum (or Kotlin object) provides the simplest, most robust solution. For legacy code, using volatile with DCL, guarding constructors, and implementing readResolve() are essential to achieve a truly single instance.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaReflectionserializationenumthread safetydesign patternSingleton
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

0 followers
Reader feedback

How this landed with the community

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.