Mastering Java Singleton: From Lazy Initialization to Enum with Thread‑Safety
This article walks through multiple Java Singleton implementations—lazy initialization, double‑checked locking, volatile‑protected, static‑inner‑class, and enum—explaining their thread‑safety characteristics, the pitfalls of instruction reordering, and how reflection and serialization can break or preserve the pattern.
First version: Lazy (non‑thread‑safe)
The basic Singleton uses a private constructor and a static field initialized to null. The getInstance() method creates the instance on first call:
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}This is called the "lazy" approach because the object is created only when needed, but it is not safe for concurrent threads.
Second version: Double‑checked locking (still unsafe without volatile )
To avoid creating multiple instances in a multithreaded environment, a synchronized block is added with a second null‑check inside:
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}The inner check is the "double‑checked" mechanism. However, without volatile, the JVM may reorder instructions, allowing another thread to see a partially constructed object.
Why instruction reordering matters
During object creation the JVM may execute the steps in the order:
memory = allocate();
instance = memory; // reference assigned before constructor runs
ctorInstance(memory);If Thread A assigns the reference before the constructor finishes, Thread B can read a non‑null reference to an incompletely initialized instance, leading to subtle bugs.
Third version: Adding volatile (safe double‑checked locking)
Marking the static field as volatile prevents reordering and guarantees visibility of the latest value across threads:
public class Singleton {
private Singleton() {}
private volatile static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}Now the three steps (allocate, construct, assign) always occur in that order, ensuring thread safety.
Static inner‑class implementation (lazy‑loaded, thread‑safe)
This technique leverages class‑loader semantics: the inner class is not loaded until getInstance() is called, guaranteeing lazy initialization without explicit synchronization.
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}Enum implementation (simplest, serialization‑safe, reflection‑proof)
Using a single‑element enum provides an inherently thread‑safe Singleton that cannot be broken by reflection or deserialization.
public enum SingletonEnum {
INSTANCE;
}Breaking a Singleton with reflection
Reflection can bypass the private constructor:
// Obtain constructor
Constructor<Singleton> con = Singleton.class.getDeclaredConstructor();
con.setAccessible(true);
// Create two distinct instances
Singleton s1 = con.newInstance();
Singleton s2 = con.newInstance();
System.out.println(s1.equals(s2)); // prints falseThis demonstrates that classic implementations are vulnerable unless additional safeguards (e.g., throwing an exception in the constructor when an instance already exists) are added.
Why enum and readResolve matter for serialization
Enum Singletons automatically preserve the singleton property during deserialization. For other implementations, defining a readResolve() method that returns the existing instance is required to avoid duplicate objects after deserialization.
Key takeaways:
Use volatile with double‑checked locking to prevent instruction reordering.
Static inner‑class and enum approaches are the most robust and concise.
Reflection can break most implementations; enums are immune.
Implement readResolve for serialization safety when not using enums.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
