You may often be asked about singletons in an interview, and if the interviewer is testing your understanding of singletons, you will most likely be asked to write singletons by hand. The singleton pattern seems simple, but if you dig deeper, you can see how well an interviewer understands the basics of concurrency, serialization, and class loading. And there are many different ways to write the singleton pattern, so you might want to know which one is better, and I’ve summarized several of them and presented them to you.
First we need to know what is a singleton pattern? The singleton pattern refers to ensuring that a class has only one instance and provides a globally accessible entry point.
So what are the common ways to write this? I think there are five. Hunchman, slacker, double-checked, static inner class, and enumeration, we go from level to level according to how easy it is to write. First of all, let’s look at the relatively simple hungry-esque. What does it look like?
/** * initialization is done when the class is loaded. */ public class Singleton { private static Singleton singleton = new Singleton(); privateSingleton() {} // The constructor is private and cannot be constructed by external classes. public static SingletongetInstance() {
returnsingleton; }}Copy the code
Render our instance static and render the constructor private. This is easier to write and completes the instantiation at class load time, avoiding thread synchronization. The downside is that the instantiation is done at class load rather than lazy loading, so if the instance is never used at all, it can be a waste of memory.
Let’s look at the second method, lazy, which is instantiated when the getInstance method is called. Our object is lazy-loaded, but can only be used in a single thread. If a thread enters an if(singleton == null) statement before it can proceed and another thread passes the statement, the instance will be created multiple times. However, it is important to note that this method cannot be used in a multi-threaded environment. This is the wrong way to write it.
/** * lazy loading. Public class Singleton {private static Singleton; privateSingleton() {
}
public static Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
returnsingleton; }}Copy the code
To solve the problem of thread insecurity, we need to use the synchronized keyword to ensure thread safety.
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if(singleton == null) { synchronized (Singleton.class) { singleton = new Singleton(); }}returnsingleton; }}Copy the code
This is thread-safe, but creates multiple instances. When two threads pass the first if statement, we continue. The first thread initializes an instance and releases the lock. The second thread also initializes an instance again. We have to do a double check.
public class Singleton {
private static volatile Singleton singleton;
private Singleton(){}
public static Singleton getInstance() {
if(singleton == null) {
synchronized (Singleton.class) {
if(singleton == null) { singleton = new Singleton(); }}}returnsingleton; }}Copy the code
Remove the first check, inefficient, the program serial execution. Removing the second check creates multiple instances. Why the volatile keyword?
singleton = new Singleton(); This phrase does at least three things.
- Allocate memory space for singleton.
- Call the constructor to initialize the Singleton.
- Point the Singleton object to the allocated memory space. (Singleton is no longer null) If there are two threads and there is emphatic ordering without volatile, our order is 1-3-2. singleton = new Singleton(); When thread 1 reaches this statement, it completes step 3, but step 2 does not execute. However, thread 2 is scheduled to execute at this time. When entering the first check, we find that the Singleton is no longer null, so we directly return an uninitialized Singleton. In this way, we will report an error when using the uninitialized Singletn.
Let’s look at the static inner class:
/** * Static inner classes are written to ensure thread-safety and lazy loading. * However, as with Double Check, there is no guarantee that deserialization will generate multiple instances. */ public class Singleton { privateSingleton(){}
private static class SingletonInstance {
private static final Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
returnSingletonInstance.singleton; }}Copy the code
You can see that static inner classes and double-checking are both good ways to write them, but they don’t prevent deserialization and generating multiple instances. Is there a better way to write them? Finally, let’s look at how to write enumerations. Implementing the singleton pattern with enumerated classes added in JDK 1.5 not only avoids thread synchronization problems, but also prevents deserialization and reflection from creating new objects that break the singleton.
public enum Singleton {
INSTANCE;
public void whateverMethod() {
System.out.println("Performs a singleton class method, such as returning environment variable information."); }} / / Test class public class Test {public static void main (String [] args) {Singleton. INSTANCE. WhateverMethod (); }}Copy the code
We’ve talked about hunchman, lazy, double-checked, static inner class, and enumerated classes. With so many methods available to implement singletons, you might be asking, how do I choose which singletons to implement?
I think the best way to do this is to use enumerated classes! Joshua Bloch makes it clear in Effective Java that enumerations are the best way to implement singletons, although they are not yet widely adopted. Why he will be more respected enumeration singleton pattern, this will have to return to the advantages of enumeration method, the enumeration method has the advantages of such a few, first of all is written in simple, the enumeration method don’t need to go our own consideration, lazy loading, thread safe, code is short at the same time, more than any other writing more concise, more elegant.
The second advantage is that it is thread safe. By decomcompiling an enumeration class, we can see that enumerations are defined and initialized by static blocks of code that are initialized when the class is loaded, whereas Java classes are loaded thread safe by the JVM. So creating an Enum of type Enum is thread-safe.
The previous methods of implementing singletons are problematic because they can be broken by deserialization, which creates multiple instances of new objects. The third advantage of enumerated classes is that they solve these problems. For serialization, Java specifically regulates the serialization of enumerations. In serialization, only the name attribute of the enumeration object is output to the result. In deserialization, the valueOf java.lang.Enum is used to find the object based on the name, rather than creating a new object. So this prevents the singleton destruction problem caused by deserialization.
Enumeration classes also have defenses against singletons destroyed by reflection. Reflection checks whether the class is an enumerated class when creating an object through newInstance and throws an exception if it is. As you can see, enumeration has a significant advantage over other implementations in preventing serialization and reflection from destroying singletons. The security issue is not trivial; once multiple instances are generated, the singleton pattern becomes completely useless. So the combination of simplicity, thread-safety, and protection against deserialization and reflection breaking singletons, the enumeration method wins out.
So just to wrap things up, today I talked about what is a singleton pattern? Its function, use and 5 classical writing methods, including hungry, lazy, double check, static inner class and enumeration. Finally, we compare and see that enumeration has advantages in writing thread-safe and avoiding serialization reflection attacks. Here we also emphasize that if the use of thread unsafe error writing method, in the case of concurrency may produce multiple cases, then not only will affect the performance, more likely to cause serious consequences such as data errors.
If you’re facing this question in an interview, you can start with the hungry-slacker style, analyze the pros and cons of each style step by step, and evolve your style. We’ll focus on double checking, why double checking is necessary, and why the volatile keyword is necessary. Finally, we’ll discuss the advantages and principles behind the writing of enumerated classes. In addition, if encountered in the work of global information class, stateless tool class and other scenarios, it is recommended to use enumeration writing method to achieve singleton mode. I hope my sharing can help you.