Hello! Welcome to the fourth module of our Low-Level Design course.
In the previous module, we sharpened our skills in identifying SOLID principle violations and, more importantly, refactoring code to adhere to them. We learned that SOLID principles are the foundation of clean, maintainable, and extensible software.
Now, we move from the foundational principles to established, time-tested recipes for solving common design problems. This module is dedicated to Creational Design Patterns. These patterns provide various mechanisms for object creation, which increase flexibility and reuse in your code. Think of them as pre-made solutions that help you instantiate objects in a controlled and manageable way, often in direct support of principles like Dependency Inversion and Open/Closed.
Today, we'll start with one of the most well-known (and sometimes controversial) creational patterns: the Singleton. Our goal is to apply the Singleton pattern, paying close attention to a critical aspect for any production-grade system: ensuring it is thread-safe in concurrent environments.
What is the Singleton Pattern?
The Singleton pattern's intent is straightforward:
- Ensure a class has only one instance.
- Provide a single, global point of access to it.
This is useful for components that are inherently unique in a system, such as a configuration manager, a logging service, or a connection pool. You wouldn't want multiple, competing logger instances or several different configuration objects floating around.
The basic implementation involves three key elements:
- A
privateconstructor to prevent external instantiation with thenewkeyword. - A
private staticfield to hold the single instance. - A
public staticmethod (commonly namedgetInstance()) that returns the single instance.
Let's explore the various ways to implement this, starting with the simplest approaches and building up to robust, thread-safe solutions.
The article "Singleton Pattern in Java" by William Achuchi will be our main guide for different implementation strategies.
Singleton Pattern in Java: A Deep Technical Analysis from Production Systems
This article provides a deep dive into various Singleton implementations in Java, discussing their pros, cons, and thread-safety implications. We will refer to the patterns shown here throughout the lesson.
Keep this article open as a reference. We will be discussing the code examples for Eager Initialization, Lazy Initialization, Double-Checked Locking, the Bill Pugh method, and the Enum Singleton.
1. Eager Initialization: The Simplest Thread-Safe Approach
The most straightforward way to implement a Singleton is to create the instance when the class is loaded.
public class EagerSingleton {
// 1. The instance is created at class-loading time.
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 2. Private constructor prevents anyone else from creating an instance.
private EagerSingleton() {}
// 3. Global access point.
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
- Pros:
- Simple: The code is easy to understand.
- Thread-Safe: The JVM guarantees that the static field
INSTANCEis initialized in a thread-safe manner during class loading. There's no risk of a race condition.
- Cons:
- No Lazy Initialization: The instance is created whether it's needed or not. If the object is resource-intensive to create and isn't always used, this can be wasteful.
2. Lazy Initialization: The Performance-Conscious (but Naive) Approach
What if we want to delay creating the instance until it's actually requested? This is called lazy initialization. Here is the naive way to do it:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
// Create instance only if it doesn't exist yet.
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
This looks good, but it has a critical flaw in a multithreaded environment. Imagine this sequence of events:
- Thread 1 calls
getInstance()and evaluatesinstance == null. It'strue, so it proceeds to enter theifblock. - The OS scheduler pauses Thread 1 right before it executes
instance = new LazySingleton();. - Thread 2 calls
getInstance(). It also evaluatesinstance == null, which is stilltruebecause Thread 1 hasn't created the object yet. - Thread 2 enters the
ifblock and creates a newLazySingletoninstance. - Thread 1 resumes and also creates a new
LazySingletoninstance.
The result? Two instances have been created, violating the core principle of the Singleton pattern. This implementation is not thread-safe.
3. Achieving Thread-Safety for Lazy Initialization
So, how do we make our lazy Singleton safe for concurrent use?
Method 1: The synchronized Method
A simple fix is to synchronize the entire getInstance() method.
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton() {}
// The 'synchronized' keyword ensures only one thread can execute this method at a time.
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
- Pros: It works. It is completely thread-safe.
- Cons: Performance. The
synchronizedkeyword forces every thread to acquire a lock before executing the method. This is necessary the first time to prevent a race condition, but every subsequent call also pays this synchronization overhead, even though theifcondition will be false. This can become a bottleneck in high-concurrency applications.
Method 2: Double-Checked Locking (DCL)
To avoid the performance hit of synchronizing every call, we can use a clever technique called Double-Checked Locking.
public class DoubleCheckedLockingSingleton {
// The 'volatile' keyword is CRUCIAL here.
private static volatile DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {}
public static DoubleCheckedLockingSingleton getInstance() {
// First check (no lock)
if (instance == null) {
// Synchronize only when the instance is null
synchronized (DoubleCheckedLockingSingleton.class) {
// Second check (inside lock)
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
}
The logic here is to avoid the expensive synchronized block unless absolutely necessary. The first if (instance == null) check is performed without a lock. Only if the instance is null do we acquire the lock and then check again to make sure another thread didn't create the instance while we were waiting for the lock.
Why is volatile so important?
This is a subtle but critical point for interviews. Without volatile, DCL is broken. To understand why, we need to look at what new DoubleCheckedLockingSingleton() actually does, and how the JVM can reorder instructions.
Thread-Safe Singleton in Java: Understanding `volatile` and Double-Checked Locking
The article 'Thread-Safe Singleton in Java: Understanding volatile and Double-Checked Locking' provides an excellent explanation of this problem. Let's focus on its breakdown of instruction reordering.
Read the sections 'Using Double-Checked Locking for Thread Safety' and 'The Role of volatile in Thread Safety'. Pay close attention to the three steps of instance creation and how they can be reordered.
As the article explains, the line instance = new Singleton() is not a single atomic operation. It can be broken down into:
- Allocate memory for the
Singletonobject. - Call the constructor to initialize the object's fields.
- Assign the reference of the newly created object to the
instancevariable.
The Java Memory Model allows the compiler or CPU to reorder steps 2 and 3 for performance. A thread could do this:
- Allocate memory.
- Assign the reference to
instance. (instanceis now non-null). - Call the constructor.
If a second thread calls getInstance() after step 2 but before step 3 completes, it will see that instance is not null and return a reference to a partially constructed, uninitialized object. This can lead to unpredictable behavior and crashes.
The volatile keyword prevents this by establishing a "happens-before" relationship, ensuring that all writes to the volatile variable are completed before any other thread can read it. It guarantees both visibility (changes are immediately visible to all threads) and prevents instruction reordering around the variable.
Test your understanding!
An interviewer shows you the Double-Checked Locking code but without the volatile keyword on the instance variable. They ask you, "What could go wrong here?" How would you explain the potential problem?
Show answer
You would explain the concept of instruction reordering. The creation of a new object isn't atomic. Without volatile, a thread might get a reference to the instance variable before the object's constructor has finished running. This means the thread would be working with a partially initialized, unsafe object, which could lead to NullPointerExceptions or other erroneous behavior when its methods are called. The volatile keyword is essential to prevent this reordering and ensure any thread that sees a non-null instance sees a fully constructed object.
4. Modern and Recommended Implementations
While DCL is a classic interview topic, modern Java offers cleaner and safer ways to achieve the same goal.
Method 3: Initialization-on-demand Holder (Bill Pugh Singleton)
This is a clever solution that leverages how the JVM handles class loading.
public class BillPughSingleton {
private BillPughSingleton() {}
// A private static inner class holds the instance.
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
// The inner class is not loaded until this method is called.
return SingletonHelper.INSTANCE;
}
}
- How it works: The static inner class
SingletonHelperis not loaded into memory until thegetInstance()method is called for the first time. The JVM guarantees that the class loading process is thread-safe. - Benefits: It achieves lazy initialization in a clean, highly readable, and thread-safe way without any explicit
synchronizedorvolatilekeywords. For lazy-initialized singletons, this is often the preferred approach.
Method 4: The Enum Singleton
Joshua Bloch, in his book "Effective Java," advocates for using an enum to implement a singleton.
public enum EnumSingleton {
INSTANCE;
// You can add methods here
public void doSomething() {
System.out.println("Enum singleton is doing something!");
}
}
- Benefits:
- Extremely simple and concise.
- Inherently thread-safe.
- Provides built-in protection against serialization and reflection attacks, which can be used to create extra instances of other singleton implementations.
- Drawback: It doesn't support lazy initialization. The
INSTANCEis created when the enum class is loaded.
The following table gives a great overview of the trade-offs.

A Note for Spring Boot Developers
Given your experience with Spring Boot, you're already very familiar with the concept of a "singleton scope." It's important to understand the difference between the classic GoF Singleton pattern we've discussed and Spring's singletons.

In a Spring application, you typically don't implement the Singleton pattern manually like this. Instead, you annotate a class with @Component (or @Service, @Repository, etc.). By default, Spring manages this class as a singleton within its container and injects the same instance wherever it's needed using @Autowired. Spring's approach is more flexible and handles the lifecycle and dependency injection for you, which aligns perfectly with the Dependency Inversion Principle.
Conclusion
Today we took a deep dive into the Singleton pattern, with a critical focus on thread-safety. While simple in concept, making it work correctly in a concurrent environment requires careful consideration.
Key Takeaways:
- The Singleton pattern guarantees a single instance of a class and a global access point.
- Eager initialization is simple and thread-safe but not lazy.
- Naive lazy initialization is not thread-safe and should be avoided.
- Double-Checked Locking (DCL) provides thread-safe lazy initialization but is complex and requires the
volatilekeyword to work correctly. - The Bill Pugh (Initialization-on-demand Holder) pattern is the preferred modern approach for lazy, thread-safe singletons.
- The Enum Singleton is the most robust and simplest method, offering protection against reflection and serialization, but it is not lazy.
- In a Spring context, you typically rely on the framework's singleton scope rather than implementing the pattern manually.
In our next lesson, we will continue our journey through creational patterns by exploring the Factory Method pattern. You'll see how it provides a powerful way to delegate object creation to subclasses, making your systems more extensible and adhering to the Open/Closed Principle.