banner
lMingyul

lMingyul

记录穿过自己的万物
jike
twitter
github
bilibili

Can I really understand the singleton pattern this time?

Learn about common design patterns, starting with the "simplest" entry-level design pattern to practice with.

Soul Searching Questions#

What is the Singleton Pattern?#

It allows only one instance of this class to exist.

What does that mean? Normally, when we create an object instance in Java, it looks like this:

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 large amounts 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 using a unique identifier. Our common implementation methods include: 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:

Output:

  • 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 that 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 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 on the IDGenerator instance, we achieve the goal of unique ID auto-increment.

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.
    • Some might suggest defining the IDGenerator instance as a global static variable to avoid multiple instantiations.
    • This relies too much on mutual agreement among programmers, which is unreliable. Are there any enforced means to allow only a single 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-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 created for different threads to use?

How to Implement the Singleton Pattern?#

First, we need to modify the IDGenerator class:

Modifications:

  • 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 there were no duplicate IDs generated by the two threads.

This is the role of the Singleton pattern:

  1. The static constant instance is initialized only once when the IDGenerator class is loaded, ensuring that there is only one IDGenerator object and AtomicLong object globally.
  2. Changing the IDGenerator constructor to private prevents other classes from accessing it, denying other external classes the ability to instantiate IDGenerator objects.
  3. Only one unique public access 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:

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 for multi-threaded synchronization issues.
  • 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 execution, it results in wasted resources.
  • Slower Class Loading: If the class initialization is complex, it may increase the time taken for 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 issues, is there a way to delay loading?
Yes, there is: Lazy Style.


Lazy Style#

Contrary to Hungry Style, it initializes the object only when it is needed. How is this done?

Modifications:

  • Change the static constant instance to a static variable, initializing it in 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?
The above code has some issues. Although we added a null check to prevent multiple instantiations of the IDGenerator object, in a multi-threaded context, if one thread is initializing the IDGenerator object while another thread calls the getInstance() method, it can 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, which leads 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 frequent locking and releasing, resulting in performance issues.

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:

Modifications: The synchronized keyword is applied at the class level, creating a class-level lock.

How does this method avoid executing the synchronized operation 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 returns directly 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 modifying the code as follows:

Output:

An anomaly occurs; removing the second null check results in the object being initialized twice.

  • We can see that both DemoTread-2 and DemoTread-1 threads almost simultaneously passed the first null check.
  • Then, DemoTread-2 acquired the lock to instantiate the object.
  • Meanwhile, what was DemoTread-1 doing? It was waiting for DemoTread-2 to release the lock. Once DemoTread-2 created the new instance, it released the lock.
  • Next, DemoTread-1 would also acquire the lock to create a new instance!

Thus, the second check if (instance == null) is necessary to confirm whether the instance is still null after the current thread acquires the lock. If it is not null, it returns the instance directly, avoiding multiple instance creation.

At this point, you might think this is the perfect implementation of the Singleton pattern?
Not yet; the code above still has some issues. 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 is immediately visible to other threads, preventing multiple instance creations.
  • 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 necessary 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 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 is only 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:

Isn't the code simple? Why can enums achieve 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 a class is loaded, the Java Virtual Machine locks the class to prevent other threads from loading it simultaneously. The lock is released only after the class has been fully loaded, 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, as all instances are created during the class loading phase and cannot be instantiated "on demand."

So, with so many implementation methods, which one is the best?
There is no best; only the most suitable. Analyze specific scenarios.


References#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.