The Singleton pattern, also known as the single-instance pattern, is a widely used software design pattern. When apply this pattern, the class must ensure that only one instance of the singleton object exists. In this article, we will explore the two primary ways to construct a singleton pattern and finally introduce a sophisticated yet concise approach.
What is the Singleton Pattern?
The Singleton pattern is a software design pattern that restricts the instantiation of a class to one “single” instance.
The core principle is: the singleton class must guarantee that only one instance of the object exists.
Constructing the Singleton Pattern
In Java, there are two main construction approaches:
- Lazy Initialization: The singleton instance is created only when it is first used.
- Eager Initialization: The singleton instance is created when the class is loaded.
In simpler terms, one delays initialization, while the other does not.
A straightforward implementation is:
1 | public class Singleton { |
This approach is simple to implement, and the instance is created when the class is loaded. But what if we want to implement lazy initialization, where the instance is created only upon its first use?
One common technique is Double-Checked Locking (DCL):
1 | public class Singleton { |
Note that this method is only reliable on JDK 5 and later because it requires the volatile keyword to prevent instruction reordering. In versions prior to JDK 5, double-checked locking could lead to unexpected behavior.
Another Singleton Pattern
A highly recommended alternative is the Lazy Initialization Holder Class Idiom:
1 | public class Singleton { |
The JVM performs class initialization after loading the class but before any thread uses it. During this stage, the JVM acquires a lock to synchronize multiple threads’ initialization of the same class. Compared to other solutions like double-checked locking, this approach is more concise and works across all compiler versions.
For a discussion on why the access level of the static final Singleton INSTANCE field is package-private, you can read: Discussion on the implementation of the Initialization-On-Demand Holder idiom.
Using Enum
Finally, the most concise way to implement a singleton is by using an Enum.
The code is extremely simple, and its usage is straightforward:
1 | public enum Singleton { |
Reflecting on the previous implementations, they all only considered standard object retrieval. However, objects can also be retrieved through serialization and reflection. For the other methods, if they implement the Serializable interface, they must override the readResolve() method:
1 | private Object readResolve() { |
Even with readResolve(), some fields might still require the transient keyword. Serialization is generally a hassle.
Regarding reflection, typical private constructors only restrict access during compilation, not at runtime. However, an enum-based singleton provides built-in protection against both serialization and reflection. To understand why, look at the decompiled code of an enum class (for reference):
1 | public abstract class Singleton extends Enum |
Enums inherit from java.lang.Enum. You can see that the singleton instance is implemented using a static initialization block.
Why can enums prevent reflection? Simply because it is an abstract class (public abstract class Singleton extends Enum), which cannot be instantiated even via reflection.
Why can they prevent serialization issues? This depends on how Java handles object serialization.
1 | // java.io.ObjectOutputStream |
When an enum is serialized, only its name is written to the stream. The deserialization process then uses that name to retrieve the existing instance:
1 | // java.io.ObjectInputStream |
Since deserialization merely calls Enum.valueOf(Class<T> enumType, String name), it retrieves the existing instance rather than creating a new one.
Tip: Be careful when serializing enums; the name of the enum constant must not change, otherwise deserialization will fail with an exception!
Recommended Reading:
- Effective Java, Item 71: Use lazy initialization judiciously.
- Core Java Volume I, Section 14.5.8: Volatile Fields.
- JLS 17.4: Memory Model
- Java Theory and Practice: Using Volatile Variables Correctly
- Double-Checked Locking and Lazy Initialization
About Me && Blog
Below are my personal details and links. I look forward to connecting and sharing knowledge with fellow developers!
- About Me: Includes my WeChat and WeChat group links.
- Blog Navigation: A guide to the content on this blog.
- Curated Android Performance Articles: A collection of must-read performance optimization articles. Self-nominations/recommendations are welcome!
- Android Performance Knowledge Planet: Join our community for more insights.
“If you want to go fast, go alone. If you want to go far, go together.”
