preface

Some time ago, WHEN I was asked about the singleton model in the interview, I failed to answer it. It was a little embarrassing and I did not pay attention to it. Just dimly remember once had seen relevant data on the net, remember a few nouns such as lazy, hungry han type. Although there is nothing wrong with being unfamiliar with design patterns as a developer with two years of experience, I think it is necessary to master simple and commonly used singleton patterns, and as a programmer who dreams of being an architect, he must be more advanced. I hope this article can be helpful to those who are not familiar with singleton patterns.

This section describes the singleton scenario

The singleton pattern means that a class in a program runs with only one object. For example, database connection pools are not created repeatedly, and Spring generates and uses a singleton Bean in the same container.

The singleton pattern mainly solves the problem that a class is used globally and not created and destroyed frequently, thus improving the overall performance of the code.

The hungry type

For example, if a person is hungry, the first thing to do is to prepare a hearty meal. The idea of hanky-hanky is to create the target object during class loading and return the instantiated object when the runtime calls the method to retrieve the instance.

public class HungrySingleton { private static final HungrySingleton SINGLETON = new HungrySingleton(); /** * The singleton pattern has a feature that does not allow external direct creation of objects, */ private HungrySingleton() {} public static HungrySingleton getInstance() {return SINGLETON; }}Copy the code

disadvantages

As long as the JVM loads the HungrySingleton class, regardless of whether or not we call getInstance, we initialize the member variable to create the instance, which takes up extra memory that we don’t need and slows down the program’s startup time.

How much space do you think this object can take up? This is just a simple example. What if there are other member variables inside this object? Take a more dramatic example

public class HungrySingleton { private static final HungrySingleton singleton = new HungrySingleton(); byte[] arr1 = new byte[1024*1024]; byte[] arr2 = new byte[1024*1024]; byte[] arr3 = new byte[1024*1024]; byte[] arr4 = new byte[1024*1024]; /** * omit... * /}Copy the code

This object takes up a lot of memory. Once instantiated, these member variables are initialized…… So usually we recommend the slacker style

LanHanShi

Spring lazy loading means that we don’t want the Bean to be created when the Spring container is started, but we don’t want the Bean to be created until we actually use it at runtime.

Lazy is the idea that the getInstance method is called at run time to create the object return, thus eliminating the problem of hungry taking up unnecessary memory and slowing down the program startup time.

public class LazySingleton { private static LazySingleton singleton; private LazySingleton(){} public static LazySingleton getInstance(){ if(singleton == null){ return new LazySingleton(); } return singleton; }}Copy the code

The code above is lazy singleton code in its simplest form, and it looks good, but it hides two important problems.

Problems in multi-threaded environment

If thread A accesses the getInstance method at the same time, thread A may decide that singleton == null is true and execute the instantiation, but the instantiation process is not finished. Thread B also determines that singleton == null is true and executes the instantiation. Multiple objects are generated, which violates the intent of the singleton pattern. So we need a lock to solve this problem

Public static LazySingleton getInstance() {synchronized (LazySingleton. Class) {synchronized (LazySingleton (singleton == null) { return new LazySingleton(); } } return singleton; }Copy the code

This code solves the problem of creating multiple Instances of LazySingleton in a multi-threaded environment, locking the Class object of the current Class so that only one thread can hold the lock at a time while executing the code inside the method. The catch, however, is that every thread that enters this method first acquires the lock, which is a performance drain. We can optimize it a little bit

public static LazySingleton getInstance() {
    if (singleton == null) {
        synchronized (LazySingleton.class) {
            if (singleton == null) {
                return new LazySingleton();
            }
        }
    }
    return singleton;
}
Copy the code

Checking the singleton before locking can significantly avoid the performance penalty of the first method of acquiring the lock no matter what, but this code can still be problematic in extreme cases due to instruction reordering.

Instruction reordering causes NPE problems

To talk about this question we need to understand two other concepts

  • Instruction reordering

To improve program performance, the compiler and processor may reorder the order in which code instructions are executed.

What does that mean? An example is the following code

a = 3;
b = 2;
a = a + 1;
Copy the code

After compilation, a line of code will correspond to one or more instructions, which means that it should be executed from top to bottom, but if reordering occurs, it might actually be executed in this order

a = 3;
a = a + 1;
b = 2;
Copy the code

There are more details about this reorder, and there will be a special article in the future. Anyway, now you can understand what this reorder is.

  • The internal process of object instantiation

Now that we understand reordering, let’s look at the steps of object instantiation. Object obj = new Object(); It’s not an atomic operation underneath. If you look at the bytecode of this line of code, it’s obviously done in multiple steps, and if you look at the official bytecode documentation what do these instructions do

0: new #2 //1. Load the class (if needed); 4: Invokespecial #1 // Call the constructor (after this step is complete, the object's heap memory is not complete) 7: Astore_1 // Stores the reference to the top of the operand stack in the local variable tableCopy the code

Annotation above lists these a few simple instructions to do (the actual details much more complicated than this), which means under normal circumstances the execution order is 0,4,7, but because of the existence of the reorder may lead to execution sequence of 0,7,4, see LanHanShi code, assuming that the first thread over 7 perform end, 4 is not over, If (singleton == null) will be set to false. If (singleton == null) will be set to false. Therefore, a NullPointerException(NPE) may be reported

Public static getInstance() {if (singleton == null) {public static getInstance() {if (singleton == null) { Return synchronized (lazysingleton.class) {if (singleton == null) {return new LazySingleton(); } } } return singleton; }Copy the code

It is worth noting the bytecode example aboveobjIt’s thread private, but I just want to give you an example of thatnewThe process is divided into many steps, lazy stylesingletonVariables are global variables. In fact, bytecode instructions are generally the same, and instruction rearrangement will occur.

Of course, solving this problem is much simpler than understanding it. We all know that using the volatile keyword directly to modify shared variables in Java prevents instructions from reordering.

private static volatile LazySingleton singleton;

Then the lazy code above is truly safe.

Static inner classes implement singletons

Since the JVM’s class loading is lazy, a class is not loaded until it is really needed, we can use this feature to implement lazy using static inner classes. Since the JVM can ensure concurrent access by multiple threads while loading classes, the synchronized keyword can be used to achieve lazy thread safety.

Public class InnerClassSingleton {/** * private static class SingletonHolder {private static final InnerClassSingleton SINGLETON = new InnerClassSingleton(); } private InnerClassSingleton() {} public static InnerClassSingleton getInstance() { return SingletonHolder.SINGLETON; }}Copy the code

This approach enables lazy thread-safety without the use of the synchronized keyword, and is a recommended approach for development.

It is worth noting that we are actually just not showing the lock in the loadClass(…) of the classloader. Synchronized is used in the method source code to implement thread safety.

Use reflection to destroy singletons

You may find that some of the schemes described above can be applied in development without malicious intervention, but thanks to Java’s powerful reflection mechanism, we can write code to break singletons. Take the static inner class implementation as an example

public static void main(String[] args) throws Exception { InnerClassSingleton instance = InnerClassSingleton.getInstance(); / / normal object Constructor < InnerClassSingleton > Constructor = InnerClassSingleton. Class. GetDeclaredConstructor (); constructor.setAccessible(true); InnerClassSingleton innerClassSingleton = constructor.newInstance(); // Reflect the created object System.out.println(instance == innerClassSingleton); //false, not the same object}Copy the code

The above code can destroy the implementation of this singleton, but this is a more serious consideration, under normal circumstances, there will not be which han batch eat full support to play…… If it is in order to prevent the application from being found by someone to invade the vulnerability into malicious code is indeed possible, once on the Internet to see a case is the use of an interface or page implanted a section of server-side code……

Use enumerations to prevent reflection destruction

Since all of the previous schemes have problems that can be brute-brute-cracked by reflection, the authors of Effective Java propose an enumeration scheme. Observe the newInstance(Object… Args) method source can be found in this line

if ((clazz.getModifiers() & Modifier.ENUM) ! = 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");Copy the code

The if operation here determines that if the object you are currently creating with reflection is of an enumerated type, an exception will be thrown and enumeration objects are not allowed to be created with reflection. Doesn’t that solve our fucking problem? And using enumerations to implement singletons, the code is extremely beautiful

/** * public enum enumleton {INSTANCE; Public void whateverToDo() {system.out.println (" whateverToDo "); }}Copy the code

Tricky enumeration construction

The interesting thing is that if you use the reflection case above to test whether you can break enumeration singletons, you might actually get an error, but not newInstance(Object… Args) method reported, but reported this NoSuchMethodException error. This is because the real constructors of enumerations are actually hidden and are not decompilated as no-argument constructs. Many people on the web have written articles or videos saying that the constructors of enumerations cannot be called by reflection, which is not the case. It’s just that common decomcompiling methods don’t see the actual enumeration constructor’s parameter list, which requires more specialized software to see.

For the above example, just use the following code to get the correct Constructor object, so that calling the newInstance() method with reflection throws the expected exception that does not allow the creation of enumerated objects with reflection.

Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingleton singleton = constructor.newInstance();
Copy the code

Enumerations realize the limitations of singletons

Because enumerations implicitly inherit Enum classes, and because Java does not support multiple inheritance, this approach is not appropriate in cases where inheritance exists. In addition, enumerations complete the creation of objects during class loading, similar to hangry.

conclusion

This article is only about a small singleton implementation, but a close reading involves a lot of value digging into the underlying knowledge. Examples include class loading, object initialization details, instruction reordering and disordering, bytecode instructions, thread safety, JMM memory model, etc.

Please like and follow this article if it’s been helpful to you. Your support is my motivation to continue to create!