Learn about common design patterns, starting with the simplest design pattern to practice.
Soul Searching Questions#
What is the Singleton Pattern?#
It only allows one unique instance of this class to exist.
What does that mean? Usually, when we create an object instance in Java, it looks like this:
If only one object instance is allowed, that means 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 large amount of data and messages that need to be processed every day. At this time, we need to distinguish a large amount of data and messages with a unique identifier. Our common implementation methods include: UUID, custom incrementing 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 incrementing ID generator. Here is the following code:
Output:
- AtomicLong is a class in Java under the
java.util.concurrent.atomic
package, 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 updated 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 objects are equal (ignoring hash collisions).
We can see that 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 will also be re-instantiated multiple times, leading to duplicate IDs, which means this ID generator cannot ensure uniqueness!
So, in what situations can 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 suggest defining the IDGenerator instance as a global static variable to avoid multiple instantiations.
- This relies on mutual agreement among programmers, which is unreliable. Are there any enforced methods to allow instantiation only once?
- In a multi-threaded scenario, if two threads execute a method simultaneously that calls for ID generation, it can easily lead to ID duplication.
Now let's look at the situation of multi-threaded concurrency:
First, we define a sample thread whose task is to call the doSomething()
method in the Main class, which generates an ID.
Output:
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:
Modification points:
- Define a static constant and assign the IDGenerator instance 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.
Then modify the external access to generate IDs:
Output:
We can see that the IDGenerator class was only initialized once by the thread DemoTread-2, and the two threads generated IDs without duplication.
This is the role of the Singleton Pattern:
- The static constant instance is initialized only once when the IDGenerator class is loaded, 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 instantiation of IDGenerator objects from outside.
- Only one unique public access method is provided 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-Check, 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 the object is needed, appearing very eager, hence the name "Hungry Style."
Classic implementation of Hungry Style:
Advantages of Hungry Style#
- Thread-Safe: The object is created when the class is loaded, preventing other threads from creating multiple objects. Thread safety is guaranteed by the JVM, requiring no additional handling of multi-thread synchronization issues.
- Simple Code
Disadvantages of Hungry Style#
- Resource Waste: The object is created when the class is loaded; if it is not used during the program's execution, 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 constructor execution in the class loading process, it cannot be caught and handled.
Since early initialization has issues, is there a way to delay loading?
Yes, there is: Lazy Style.
Lazy Style#
In contrast to Hungry Style, it waits until the object is needed before initializing it. How is this done?
Modification points:
- Change the static constant instance to a static variable, and move the initialization to the
getInstance()
method. - Add a null check in the
getInstance()
method; if the instance variable is null, instantiate it.
Output:
Why are 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 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.
Advantages of Lazy Style#
- It allows for delayed loading, creating the object only when needed, avoiding unnecessary resource waste.
Disadvantages of Lazy Style#
- The need to add the synchronized keyword to ensure thread safety leads to issues such as frequent locking, releasing locks, and low concurrency, which can affect performance.
Is there a way to have the best of both worlds, with delayed loading and no performance issues? Yes, with Double-Check!
Double-Check#
Let's look at the code directly:
Modification points: Change the synchronized method to synchronized at the class level, implementing 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, avoiding synchronized initialization.
This solves the problem 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 changing the code to the following:
Output:
An anomaly occurs; without the second null check, the object was initialized twice.
- We can see that both DemoTread-2 and DemoTread-1 threads passed the first null check almost simultaneously.
- Then, the DemoTread-2 thread acquired the lock to instantiate the object.
- Meanwhile, the DemoTread-1 thread is waiting for DemoTread-2 to release the lock. After DemoTread-2 creates a new instance, it releases the lock.
- Next, the DemoTread-1 thread also acquires 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 the perfect implementation of the Singleton Pattern, but it’s not. The above code still has an issue. We need to add the volatile
keyword to the static variable instance. What is the purpose of this?
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 generations.
- Prevents Instruction Reordering: The JVM may optimize code by reordering the initialization of the object and the assignment of the instance reference, leading other threads to see a non-null object that has not yet completed initialization (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-Check method solves the problem, but it involves synchronization and checks, making it a bit complex. Is there a simpler code 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:
How does this ensure singleton implementation?
- Based on the characteristics of static inner classes, they are only loaded once, ensuring that INSTANCE is initialized only once.
- It also guarantees lazy loading, as it will only be loaded when the
getInstance()
method is called.
Will there be thread safety issues? No, because the loading of static inner classes is managed by the JVM, ensuring thread safety.
Using the static inner class method to implement singleton is efficient, avoids unnecessary thread synchronization operations, 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 enums!
Enum#
Let's look at the code directly:
See? The code is quite simple. Why can enums implement singletons?
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 is fully loaded 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 thereafter, ensuring uniqueness.
However, while this implementation looks simple, it has its downsides: it does not support lazy loading since all instances are instantiated 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.