Singleton Pattern: 6 Implementations, DCL Volatile Pitfall, Spring vs GoF
The article examines the evolution of six singleton implementations in Java, explains why the double‑checked locking pattern requires the volatile keyword to prevent instruction reordering bugs, compares Spring container‑managed singletons with classic GoF singletons, and highlights common pitfalls such as mutable state in singleton beans.
When Does a Real‑World Project Actually Need a Singleton?
In practice, singleton objects fall into two categories:
Expensive-to‑create resources such as HikariPool, RedissonClient, or ThreadPoolExecutor. Creating them repeatedly can cost hundreds of milliseconds and may crash the system.
Globally consistent state like configuration managers or logging systems, where all parts of the application must see the same data.
Understanding these scenarios gives singleton a concrete purpose beyond a textbook definition.
Six Evolutionary Implementations
Eager Initialization (Hungry Singleton)
public class ConfigManager {
// Initialized when the class is loaded; JVM guarantees thread safety
private static final ConfigManager INSTANCE = new ConfigManager();
private ConfigManager() {
loadConfig(); // read configuration file
}
public static ConfigManager getInstance() {
return INSTANCE;
}
}Simple and thread‑safe, but the instance is created at class‑loading time, which may waste resources if the object is heavy and rarely used.
Lazy Initialization (Unsafe)
public class ConfigManager {
private static ConfigManager instance;
private ConfigManager() {}
public static ConfigManager getInstance() {
if (instance == null) { // multiple threads may enter here
instance = new ConfigManager(); // possible multiple instances
}
return instance;
}
}In a multithreaded environment, two threads can both see instance == null and create separate objects, breaking the singleton guarantee.
Synchronized Getter
public static synchronized ConfigManager getInstance() {
if (instance == null) {
instance = new ConfigManager();
}
return instance;
}Thread‑safe, but every call acquires a lock even after the instance is created, causing unnecessary contention in high‑concurrency scenarios.
Double‑Checked Locking (DCL)
public class ConfigManager {
private static ConfigManager instance; // ⚠️ missing volatile – problematic
private ConfigManager() {}
public static ConfigManager getInstance() {
if (instance == null) { // first check without lock
synchronized (ConfigManager.class) {
if (instance == null) { // second check with lock
instance = new ConfigManager();
}
}
}
return instance;
}
}Most developers stop here, but without volatile the three‑step object creation ( 1. allocate memory, 2. invoke constructor, 3. assign reference) can be reordered to 1 → 3 → 2. This allows one thread to see a partially constructed object, leading to subtle bugs that only appear under specific timing.
The correct DCL adds volatile to the instance field, which (1) forbids instruction reordering and (2) guarantees that writes become visible to other threads immediately, ensuring the sequence 1 → 2 → 3.
// ✅ Correct DCL declaration
private static volatile ConfigManager instance;Static Inner‑Class (Preferred Lazy Loading)
public class ConfigManager {
private ConfigManager() {}
private static class Holder {
// Initialized only when getInstance() is first called; class loading is thread‑safe
private static final ConfigManager INSTANCE = new ConfigManager();
}
public static ConfigManager getInstance() {
return Holder.INSTANCE;
}
}This leverages the JVM’s class‑initialization lock ( <clinit>) to achieve lazy loading without synchronized or volatile. It is simple, efficient, and avoids the mental overhead of DCL.
Enum Singleton (Attack‑Proof)
public enum ConfigManager {
INSTANCE;
private final Map<String, String> config;
ConfigManager() {
config = loadConfig();
}
public String get(String key) {
return config.get(key);
}
}
// Usage
ConfigManager.INSTANCE.get("db.url");The enum approach eliminates both the reflection attack (the JVM blocks newInstance() on enums) and the serialization issue (deserialization returns the existing enum constant). Joshua Bloch cites this as the best way to implement a singleton in Effective Java .
The Most Common Pitfall: Mutable State in a Singleton
@Service
public class OrderExportService {
// 💣 Dangerous mutable field shared by all threads
private List<Long> processedOrderIds = new ArrayList<>();
public ExportResult export(ExportRequest request) {
processedOrderIds.clear();
List<Order> orders = orderDao.query(request);
for (Order order : orders) {
doExport(order);
processedOrderIds.add(order.getId()); // record processed IDs
}
return ExportResult.success(processedOrderIds);
}
}When two requests run concurrently, both threads may clear the list and then add IDs simultaneously, leading to mixed data or ConcurrentModificationException. Because a singleton’s fields are shared across all threads, storing mutable business state effectively creates a global variable.
Fix: move the mutable data to a method‑local variable so each thread gets its own copy.
@Service
public class OrderExportService {
public ExportResult export(ExportRequest request) {
List<Long> processedOrderIds = new ArrayList<>(); // local, thread‑exclusive
List<Order> orders = orderDao.query(request);
for (Order order : orders) {
doExport(order);
processedOrderIds.add(order.getId());
}
return ExportResult.success(processedOrderIds);
}
}In Spring beans, only @Autowired dependencies (which are themselves stateless singletons) should be stored as fields; temporary data belongs in local variables.
Spring Singleton vs. GoF Singleton
Scope : GoF singleton lives for the entire JVM (same ClassLoader); Spring singleton lives within a Spring ApplicationContext.
Enforcement : GoF relies on a private constructor and static field; Spring relies on container management, but you can still instantiate objects with new outside the container.
Multiple Containers : GoF is unaffected; Spring creates a separate instance per ApplicationContext.
Essence : GoF is a language‑level constraint; Spring is a lifecycle managed by the container.
The most critical point: @Scope("singleton") in Spring means a single instance per container, not per JVM. Manually creating an object with new UserService() bypasses Spring’s singleton management.
In real projects, most beans are Spring‑managed singletons, and the GoF pattern is rarely needed.
Key Takeaways for Interviews
When asked about DCL, always mention that volatile prevents instruction reordering and guarantees visibility.
For hand‑written singletons, prefer the static‑inner‑class approach; use the enum approach when you need protection against reflection and serialization attacks.
Never store mutable state in a Spring singleton; keep such data in method‑local variables.
Understand that Spring’s singleton scope differs from the classic GoF singleton scope.
折腾派程序员 focuses on Java backend, AI engineering, and architecture design. Follow for more technical deep dives.
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.
Tinker Programmer
Solving problems with code, sharing practical tech insights, and leveling up together!
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.
