The singleton pattern is one of the most popular design patterns in interviews. Interviewers often ask you to write down two types of singleton patterns and explain their principles. Without further discussion, let’s begin to learn how to answer this interview question well.
What is the singleton pattern
When the interviewer asks what a singleton is, do not answer the question by saying there are two types of singleton. Focus on the definition of the singleton.
A singleton is a design pattern in which objects are created in memory only once. The singleton pattern allows a program to create only one object in memory, sharing it with all the places that need to be called, to prevent memory from exploding when the same object is used multiple times in the program.
The type of singleton pattern
There are two types of singleton patterns:
LanHanShi
: Create the singleton class object only when you really need to use itThe hungry type
: This singleton is already created at class load time and is waiting to be used by the program
Lazy creation of singletons
The lazy method of creating an object is to determine whether the object has been instantiated (nulled) before the program uses it and return the object if it has been instantiated. Otherwise, perform the instantiation operation first.
From the flowchart above, you can write the following code
public class Singleton {
private static Singleton singleton;
private Singleton(a){}
public static Singleton getInstance(a) {
if (singleton == null) {
singleton = new Singleton();
}
returnsingleton; }}Copy the code
Yes, we’ve written a nice singleton pattern here, but it’s not perfect, but that doesn’t stop us from using the singleton.
That’s the lazy way to create a singleton, and I’ll explain where this code can be optimized and what the problem is.
Create a singleton
The hander creates the object when the class is loaded and returns the singleton when the program is called. That is, we specify when we code that we need to create the object immediately instead of waiting until it is called.
With regard to class loading, as far as the JVM is concerned, we can now simply assume that the singleton object is created when the program starts.
public class Singleton{
private static final Singleton singleton = new Singleton();
private Singleton(a){}
public static Singleton getInstance(a) {
returnsingleton; }}Copy the code
Note that the above code instantiates a Singleton object in line 3. There are no more instances of Singleton objects in memory
When the class is loaded, a Singleton object is created in the heap memory, and when the class is unloaded, the Singleton object dies.
How does lazy ensure that only one object is created
Let’s revisit the lazy core approach
public static Singleton getInstance(a) {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
Copy the code
The problem with this approach is that if both threads decide that a Singleton is empty, they will both instantiate a Singleton object, which will become multiple instances. So, we need to solve the thread safety problem.
The most obvious solution is to lock a method, or a class object, and the program will look something like this
public static synchronized Singleton getInstance(a) {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
/ / or
public static Singleton getInstance(a) {
synchronized(Singleton.class) {
if (singleton == null) {
singleton = newSingleton(); }}return singleton;
}
Copy the code
This avoids the risk of two threads creating Singleton objects at the same time, but introduces another problem: each attempt to acquire the object requires the lock to be acquired first. Concurrency performance can be very poor, and in extreme cases, stuttering can occur.
The next step is to optimize performance: the goal is to lock the object if it is not instantiated, and if it is instantiated, to get the instance without locking it
So locking directly on a method is deprecated because it requires obtaining the lock anyway, right
public static Singleton getInstance(a) {
if (singleton == null) { Thread A and thread B both see singleton = null, if not null, return singleton directly
synchronized(Singleton.class) { // Thread A or thread B acquires the lock for initialization
if (singleton == null) { // One thread enters the branch and the other thread does not
singleton = newSingleton(); }}}return singleton;
}
Copy the code
The code above perfectly solves the concurrency safety + performance inefficiency problem:
- In line 2, if the singleton is not empty, the object is returned without the need to acquire the lock; If multiple threads find the Singleton empty, they branch.
- In line 3, multiple threads attempt to acquire the same lock, and only one thread succeeds. The first thread to acquire the lock will again determine whether the singleton is empty, since it may have been instantiated by the previous thread
- The other thread that later acquired the lock will execute the verification code at line 4 and find that the Singleton is not empty, so it will not create another object and will directly return the object
- All subsequent threads that enter the method do not acquire the lock, and the Singleton object is no longer empty when first judged
This lazy notation is also called Double Check + Lock because it requires two nulls and locks on class objects.
The complete code looks like this:
public class Singleton {
private static Singleton singleton;
private Singleton(a){}
public static Singleton getInstance(a) {
if (singleton == null) { Thread A and thread B both see singleton = null, if not null, return singleton directly
synchronized(Singleton.class) { // Thread A or thread B acquires the lock for initialization
if (singleton == null) { // One thread enters the branch and the other thread does not
singleton = newSingleton(); }}}returnsingleton; }}Copy the code
The above code is nearly perfect, but there is one final problem: instruction rearrangement
Use volatile to prevent instruction reordering
To create an object, there are three steps in the JVM:
(1) Allocate memory space for singleton
(2) Initialize the Singleton object
(3) Point the singleton to the allocated memory space
Instruction reordering is the ability of the JVM to execute statements in a different order than they were coded to maximize program performance while ensuring that the final result is correct
In the three steps, instruction rearrangement may occur in steps 2 and 3, and the order of object creation is changed to 1-3-2. As A result, when multiple threads acquire objects, thread A may execute steps 1 and 3 during object creation, and thread B may judge that the singleton is no longer empty. An NPE exception is reported when an uninitialized Singleton object is obtained. The text is more obscure, you can see the flow chart:
The use of the volatile keyword prevents instruction reordering. This article does not attempt to explain the complex mechanism. Volatile variables ensure that instructions are executed in the same order as specified by the program, preventing NPE exceptions in multithreaded environments.
Volatile also serves a second purpose: Variables decorated with the volatile keyword are visible in memory. That is, each time a thread reads the variable, it must read the variable first.
The final code looks like this:
public class Singleton {
private static volatile Singleton singleton;
private Singleton(a){}
public static Singleton getInstance(a) {
if (singleton == null) { Thread A and thread B both see singleton = null, if not null, return singleton directly
synchronized(Singleton.class) { // Thread A or thread B acquires the lock for initialization
if (singleton == null) { // One thread enters the branch and the other thread does not
singleton = newSingleton(); }}}returnsingleton; }}Copy the code
Break slacker singleton and hungry singleton
Both the perfect slacker and hunchman can be defeated by reflection and serialization, both of which can destroy singletons (creating multiple objects).
usingreflectionBreak the singleton pattern
Here is an example using the reflection destruction singleton pattern
public static void main(String[] args) {
// Get the explicit constructor for the class
Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
// Access private constructors
construct.setAccessible(true);
// Use reflection to construct a new object
Singleton obj1 = construct.newInstance();
// Get the singleton in the normal way
Singleton obj2 = Singleton.getInstance();
System.out.println(obj1 == obj2); // false
}
Copy the code
The above code hits the nail on the head: use reflection to force access to the class’s private constructor to create another object
usingSerialization and deserializationBreak the singleton pattern
Here is an example of breaking the singleton pattern using serialization and deserialization
public static void main(String[] args) {
// Create an output stream
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// Write the singleton to the file
oos.writeObject(Singleton.getInstance());
// Reads a singleton from a file
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// Check if it is the same object
System.out.println(newInstance == Singleton.getInstance()); // false
}
Copy the code
The reason the two object addresses are not equal is that when the readObject() method reads an object, it must return a new object instance that points to a new memory address.
Let the interviewer clap enumeration implementation
We’ve already seen the slacker and hungrier forms, and that’s usually enough for now. But how can we stop there? The book Effective Java provides the ultimate solution. Without further ado, you will be tested by your interview manager.
After JDK 1.5, there was another way to implement the singleton pattern using the Java language: enumerations
Enumeration to implement the singleton pattern complete code as follows:
public enum Singleton {
INSTANCE;
public void doSomething(a) {
System.out.println("This is a singleton pattern of enumerated types!"); }}Copy the code
Using enumerations to implement the singleton pattern has three advantages over the other two implementations, which let’s examine in detail.
Advantage 1: Code at a glance
The code is much more concise than the hungrier and slacker. It takes at least three lines of code to complete a singleton pattern:
public enum Test {
INSTANCE;
}
Copy the code
Let’s start with the most intuitive place. The first three lines of code we see are less. Yes, it’s less.
Advantage 2: Natural thread safety and single instance
It does not require any additional operations to ensure object singleness and thread-safety.
I have written a test code below to prove that only one Singleton object is created when the program starts, and that it is thread-safe.
We can simply understand how enumerations create instances: when the program starts, Singleton’s empty parameter constructor is called, a Singleton object is instantiated and assigned to INSTANCE, and it is never instantiated again
public enum Singleton {
INSTANCE;
Singleton() { System.out.println("Enumeration creates object."); }
public static void main(String[] args) { /* test(); * / }
public void test(a) {
Singleton t1 = Singleton.INSTANCE;
Singleton t2 = Singleton.INSTANCE;
System.out.print("Are the addresses of T1 and T2 the same:"+ t1 == t2); }}// Enumeration creates the object
// Whether the addresses of t1 and T2 are the same: true
Copy the code
In addition to advantages 1 and 2, a final advantage is the protection of the singleton pattern, which makes enumerations invulnerable in the current singleton pattern realm
Advantage 3: Enumeration protects the singleton pattern from being broken
Using enumerations prevents callers from breaking the singleton pattern by forcing multiple singletons to be generated using reflection, serialization, and deserialization mechanisms.
antireflection
Enumeration classes inherit from Enum classes by default, and when newInstance() is called with reflection, it determines whether the class is an enumeration class and throws an exception if it is.
Prevents deserialization from creating multiple enumerated objects
Each enumeration type and enumeration name is unique when the Singleton object is read in, so only the enumeration type and variable name are printed to the file at serialization time. When the file is read in and deserialized into the object, Use the valueOf(String Name) method of the Enum class to find the corresponding enumeration object based on the name of the variable.
So, during serialization and deserialization, only enumeration types and names are written out and read in, and nothing is done about the object.
Summary:
(1) Use Enum type determination to prevent multiple objects from being created through reflection
(2) Enum classes serialize (deserialize) objects by writing out (read in) the object type and enumeration name. ValueOf () matches the enumeration name to find a unique object instance in memory, preventing the construction of multiple objects through deserialization
(3) Enumeration classes do not need to worry about thread safety, breaking singletons, and performance issues, because they create objects in the same way that hunhanian singletons do.
conclusion
(1) The singleton pattern is commonly written in two ways: lazy and hungry
(2) lazy: instantiate the object only when it is needed. The correct implementation is: Double Check + Lock, which solves the problem of concurrency security and low performance
(3) Hungry style: the singleton object has been created during class loading, and the object can be returned directly when obtaining the singleton object, without concurrency safety and performance problems.
(4) If the memory requirements are very high during development, then the lazy style can be used to create the object at a specific time;
(5) If the memory requirements are not high, use hungry Chinese writing method, because it is simple and error-prone, and there is no concurrency safety and performance problems
(6) In order to prevent the instruction reordering in multithreaded environment, the variable reported to NPE should be added to the singleton to prevent the instruction reordering
(7) The most elegant implementation is to use enumerations, which are compact in code, have no thread-safety issues, and are built into Enum classes to prevent reflection and deserialization from breaking singletons.