Learn about common design patterns, starting with the simplest design pattern to practice.
Soul Searching Questions#
What is the Singleton Pattern?#
It only allows for one unique instance of this class to exist.
What does that mean? Normally, when we create an object instance in Java, it looks like this:
// ClassName instanceName = new ClassName();
Demo demo = new Demo();
If only one object instance is allowed, then new Demo()
can only be called once.
How can we ensure that? By using the Singleton pattern!
Why do we need the Singleton Pattern?#
In what situations do we need only one object?
Consider this question: In a complex system, there are a lot of data and messages to process every day. At this point, we need to differentiate a large amount of data and messages with a unique identifier. The common implementations we use are: UUID, custom incremental ID generators, etc.
Using UUID-generated IDs is too long and not easy to store. Since we are studying the Singleton pattern, let's consider how to implement a custom incremental ID generator. Here’s some code to look at:
public static void main(String[] args) {
IDGenerator idGenerator = new IDGenerator();
long firstId = idGenerator.getId();
long secondId = idGenerator.getId();
printWithTreadName("firstId: " + firstId);
printWithTreadName("secondId: " + secondId);
}
public static void printWithTreadName(String msg) {
System.out.println("TreadName: " + Thread.currentThread().getName() + ", " + msg);
}
public class IDGenerator {
private AtomicLong id = new AtomicLong(0);
public IDGenerator() {
printWithTreadName("IDGenerator init");
printWithTreadName("id hashcode: " + System.identityHashCode(id));
printWithTreadName("id value: " + id.get());
}
public long getId() {
return id.incrementAndGet();
}
}
Output:
TreadName: main, IDGenerator init
TreadName: main, id hashcode: 1808253012
TreadName: main, id value: 0
TreadName: main, firstId: 1
TreadName: main, secondId: 2
- AtomicLong is a class in Java's
java.util.concurrent.atomic
package that is used for atomic operations on long values. AtomicLong uses an underlying lock-free mechanism (known as CAS, Compare and Swap in Java) to achieve concurrent control over a long variable. This means multiple threads can safely operate on AtomicLong without using synchronized or Lock. - incrementAndGet is a method of AtomicLong that atomically increments the current value by 1 and returns the new value. Atomicity ensures that this increment operation will not be interrupted by other threads in a multi-threaded environment.
- System.identityHashCode returns an integer hash code based on the object address (but not the actual memory address), which can be used to determine if two objects are equal (ignoring hash collisions).
As we can see, when the IDGenerator class is instantiated, the AtomicLong class is also instantiated, and the value of id is 0. Then, by calling the getId()
method twice through the IDGenerator instance, we achieve the goal of unique ID auto-incrementation.
But what happens if this IDGenerator is instantiated multiple times? The AtomicLong class would also be re-instantiated multiple times, leading to duplicate IDs, which means this ID generator cannot provide uniqueness!
So, under what circumstances would the IDGenerator class be instantiated multiple times?
- Different developers may not understand the actual usage of IDGenerator and instantiate it directly for use.
- Some might say to define the IDGenerator instance as a global static variable, which would avoid multiple instantiations of IDGenerator.
- This relies on mutual agreement among programmers, which is unreliable. Is there a more enforced method to allow only one instantiation?
- In a multi-threaded scenario, if two threads execute a method that calls for ID generation simultaneously, it can easily lead to ID duplication.
Now, let's look at the situation of multi-threading concurrency:
public class Main {
public static void main(String[] args) {
testWithoutSingleton();
}
public static void testWithoutSingleton() {
// Create 2 threads
DemoTread t1 = new DemoTread();
DemoTread t2 = new DemoTread();
t1.start();
t2.start();
}
public static void doSomething() {
printWithTreadName("generator id: " + new IDGenerator().getId());
}
public static void printWithTreadName(String msg) {
System.out.println("TreadName: " + Thread.currentThread().getName() + ", " + msg);
}
}
public class DemoTread extends Thread {
static int threadNo = 1;
public DemoTread () {
super("DemoTread-" + threadNo++);
}
@Override
public void run () {
doSomething();
}
}
First, we define a sample thread, where the task executed by the thread is to call the doSomething()
method in the Main class, and the doSomething()
method generates an ID.
Output:
TreadName: DemoTread-2, IDGenerator init
TreadName: DemoTread-1, IDGenerator init
TreadName: DemoTread-2, id hashcode: 359513719
TreadName: DemoTread-1, id hashcode: 2107584424
TreadName: DemoTread-1, id value: 0
TreadName: DemoTread-2, id value: 0
TreadName: DemoTread-1, generator id: 1
TreadName: DemoTread-2, generator id: 1
From the output, we can see:
- Different hashcodes indicate that the two threads generated two different AtomicLong class objects.
- The generated IDs are the same.
Two different threads generated two instances of the IDGenerator class -> leading to two different AtomicLong class objects -> resulting in duplicate IDs.
So how does the Singleton pattern ensure that only one instance is generated for different threads to use?
How to Implement the Singleton Pattern?#
First, we need to modify the IDGenerator class:
public class IDGenerator {
private AtomicLong id = new AtomicLong(0);
// Initialize the unique IDGenerator instance object
private static final IDGenerator instance = new IDGenerator();
// Change the constructor to private
private IDGenerator() {
printWithTreadName("IDGenerator init");
printWithTreadName("id hashcode: " + System.identityHashCode(id));
printWithTreadName("id value: " + id.get());
}
// Add a public method to access the unique IDGenerator instance object
public static IDGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
Changes made:
- Define a static constant and assign the IDGenerator instance object to this static constant, noting that it is a constant marked with
final
. - Change the IDGenerator constructor to private.
- Add a public method to return the unique IDGenerator instance object.
Then modify the way to access and call the ID generation:
public static void doSomething() {
printWithTreadName("generator id: " + IDGenerator.getInstance().getId());
}
Output:
TreadName: DemoTread-2, IDGenerator init
TreadName: DemoTread-2, id hashcode: 359513719
TreadName: DemoTread-2, id value: 0
TreadName: DemoTread-2, generator id: 1
TreadName: DemoTread-1, generator id: 2
We can see that the IDGenerator class was only initialized once by the thread DemoTread-2, and there were no duplicate IDs generated by the two threads.
This is the role of the Singleton pattern:
- The static constant instance is initialized only once when the IDGenerator class is loaded, and it cannot be modified, ensuring that there is only one IDGenerator object and one AtomicLong object globally.
- Changing the IDGenerator constructor to private prevents other classes from accessing it, rejecting the situation where other external classes actively instantiate the IDGenerator object.
- Only one unique public access static method is provided for external calls to return the unique IDGenerator object.
In simple terms: I won’t let you create it; I’ll create a unique object, and you can use this one.
Different Ways to Implement Singleton#
There are many ways to implement a Singleton, including: Hungry Style, Lazy Style, Double-Checked Locking, Static Inner Class, and Enum.
Hungry Style#
The implementation above is actually the Hungry Style. Why is it called Hungry Style?
Because the object is created when the class is loaded, it is initialized before it is needed, appearing very eager, hence the name "Hungry Style."
Classic Hungry Style implementation:
public class IDGenerator {
private static final IDGenerator instance = new IDGenerator();
private IDGenerator() {}
public static IDGenerator getInstance() {
return instance;
}
}
Advantages of Hungry Style#
- Thread-Safe: The object is created when the class is loaded, so no other thread can create multiple objects. Thread safety is guaranteed by the JVM, and there is no need for additional multi-threaded synchronization.
- Simple Code
Disadvantages of Hungry Style#
- Resource Waste: The object is created when the class is loaded, so if it is not used during the program's runtime, it leads to resource waste.
- Slower Class Loading: If the initialization operation of this class is complex, it may increase the time spent on class loading, affecting program startup speed.
- Cannot Handle Sudden Exceptions: If an exception is thrown during the class loading process, it cannot be caught and handled.
Since early initialization has its issues, is there a way to delay loading? Yes, the Lazy Style.
Lazy Style#
Contrary to the Hungry Style, it waits until the object is needed before initializing. How do we do that?
public class IDGenerator {
private AtomicLong id = new AtomicLong(0);
private static IDGenerator instance;
private IDGenerator() {
Instant instant = Instant.ofEpochMilli(System.currentTimeMillis());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
String formattedTime = formatter.format(instant);
printWithTreadName("Current time: " + formattedTime);
printWithTreadName("IDGenerator init");
printWithTreadName("id hashcode: " + System.identityHashCode(id));
printWithTreadName("id value: " + id.get());
}
public static IDGenerator getInstance() {
if (instance == null) {
instance = new IDGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
Changes made:
- Change the static constant instance to a static variable, moving the variable initialization to the
getInstance()
method. - Add a null check in the
getInstance()
method; if the instance variable is null, it will instantiate it.
Output:
TreadName: DemoTread-2, Current time: 2023-07-25 21:28:09
TreadName: DemoTread-2, IDGenerator init
TreadName: DemoTread-1, Current time: 2023-07-25 21:28:09
TreadName: DemoTread-1, IDGenerator init
TreadName: DemoTread-1, id hashcode: 1457402119
TreadName: DemoTread-2, id hashcode: 822717100
TreadName: DemoTread-2, id value: 0
TreadName: DemoTread-2, generator id: 1
TreadName: DemoTread-1, generator id: 1
Oh, why are there two instance objects generated?
Actually, the above code has a problem. It seems that we added a null check to prevent multiple instantiations of the IDGenerator object, but in a multi-threaded scenario, if one thread is initializing the IDGenerator object and has not completed, while another thread calls the getInstance()
method, it will lead to multiple initializations of the IDGenerator object.
From the output, we can see that both DemoTread-2 and DemoTread-1 threads started initializing the IDGenerator object at 2023-07-25 21:28:09
, leading to an error.
How can we avoid this? By adding the synchronized
keyword to the getInstance()
method, we ensure that only one thread can access this method at a time.
public static synchronized IDGenerator getInstance() {
if (instance == null) {
instance = new IDGenerator();
}
return instance;
}
Advantages of Lazy Style#
- It allows for delayed loading, creating the object only when needed, thus avoiding unnecessary resource waste.
Disadvantages of Lazy Style#
- The need to add the synchronized keyword to ensure thread safety leads to issues like frequent locking and unlocking, low concurrency, and performance impact.
Is there a way to achieve both delayed loading and avoid performance issues? Yes, with Double-Checked Locking!
Double-Checked Locking#
Let’s look at the code directly:
public static IDGenerator getInstance() {
if (instance == null) {
synchronized (IDGenerator.class) {
if (instance == null) {
instance = new IDGenerator();
}
}
}
return instance;
}
Changes made: The synchronized modifier on the method is changed to a class-level lock.
How does this method avoid executing synchronized multiple times?
- First, when a thread enters the
getInstance()
method, it checks the current state: there are only two states, either the IDGenerator has been fully instantiated (current instance is not null) or it has not been instantiated (current instance is null).- If the instance is null, it performs a synchronized initialization operation to instantiate the object.
- If the instance is not null, it directly returns it without performing synchronized initialization.
This resolves the issue of the Lazy Style requiring synchronization on every call.
What is the purpose of the first null check? What about the second null check? Can we remove it? Let’s try. We’ll change the code to:
public static IDGenerator getInstance() {
if (instance == null) {
printWithTreadName("Entering first null check");
synchronized (IDGenerator.class) {
printWithTreadName("Entering synchronized block");
instance = new IDGenerator();
}
}
return instance;
}
Output:
TreadName: DemoTread-2, Entering first null check
TreadName: DemoTread-1, Entering first null check
TreadName: DemoTread-2, Entering synchronized block
TreadName: DemoTread-2, Current time: 2023-07-26 00:48:24
TreadName: DemoTread-2, IDGenerator init
TreadName: DemoTread-2, id hashcode: 26262847
TreadName: DemoTread-2, id value: 0
TreadName: DemoTread-1, Entering synchronized block
TreadName: DemoTread-1, Current time: 2023-07-26 00:48:24
TreadName: DemoTread-1, IDGenerator init
TreadName: DemoTread-1, id hashcode: 1846522864
TreadName: DemoTread-1, id value: 0
TreadName: DemoTread-1, generator id: 1
An anomaly occurs; removing the second null check leads to the object being initialized twice.
- It can be seen that both DemoTread-2 and DemoTread-1 threads almost simultaneously passed the first null check.
- Then, the DemoTread-2 thread acquired the lock to instantiate the object.
- Meanwhile, what is the DemoTread-1 thread doing? It is waiting for DemoTread-2 to release the lock. Once DemoTread-2 creates the new instance and releases the lock, the DemoTread-1 thread will also acquire the lock to create a new instance!
Thus, the second check if (instance == null)
is to confirm again whether the instance is still null after the current thread acquires the lock. If it is not null, it directly returns the instance, avoiding the creation of multiple instances.
Now, you might think this is a perfect implementation of the Singleton pattern, but it’s not. The code still has an issue. We need to add the volatile
keyword to the static variable instance. What is the purpose of this?
private static volatile IDGenerator instance;
It ensures the visibility and prevents instruction reordering of this static variable.
- Visibility: The volatile keyword ensures that a value written by one thread will be immediately visible to other threads, preventing multiple instance objects from being generated.
- Prevents Instruction Reordering: The JVM may optimize the code by reordering the initialization of the object and the assignment of the instance reference, leading to other threads reading the instance as a non-null object that has not yet completed initialization (executing the remaining logic in the constructor). If other threads use it directly, they will be using an incomplete object, so the volatile keyword is needed to prevent instruction reordering.
Static Inner Class#
The Double-Checked Locking method solves the problem but involves synchronization and checks, making it a bit complex. Is there a simpler implementation? Yes, using a static inner class.
The specific implementation is to define another static inner class within the class that needs to implement the Singleton:
public class IDGenerator {
private AtomicLong id = new AtomicLong(0);
private static class SingletonHolder {
private static final IDGenerator INSTANCE = new IDGenerator();
}
private IDGenerator() {
printWithTreadName("IDGenerator init");
printWithTreadName("id hashcode: " + System.identityHashCode(id));
printWithTreadName("id value: " + id.get());
}
public static IDGenerator getInstance() {
return SingletonHolder.INSTANCE;
}
public long getId() {
return id.incrementAndGet();
}
}
How does this ensure a Singleton implementation?
- Based on the characteristics of static inner classes, it will only be loaded once, ensuring that INSTANCE is initialized only once.
- It also guarantees lazy loading because the static inner class is loaded only when the
getInstance()
method is called.
Will there be thread safety issues? No, because the loading of static inner classes is handled by the JVM, making it thread-safe.
Using the static inner class method to implement Singleton is efficient, avoids unnecessary thread synchronization, achieves lazy loading, and ensures thread safety—perfect!
The only downside is that it requires defining an additional class. Is there an even simpler way? Yes, with Enum!
Enum#
Let’s look at the code directly:
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
See, the code is quite simple. Why can an enum implement Singleton?
Because Java uses class loading mechanisms to ensure the uniqueness and thread safety of enum types.
- Thread Safety: During the class loading process, when loading a class, the Java Virtual Machine locks the class to prevent other threads from loading it simultaneously. Only after the class loading is complete is the lock released, ensuring thread safety.
- Uniqueness: Instances of enum types are created all at once during the loading of the enum type and will not change afterward, ensuring uniqueness.
However, while this implementation looks simple, it has its downsides: it does not support lazy loading since all instances are created during the class loading phase and cannot be "on-demand."
So, with so many implementation methods, which one is the best?
There is no best; only the most suitable. Analyze specific scenarios.