Of the 23 design patterns, the singleton pattern is probably the most commonly used. Although this design pattern is familiar and simple, there are still some things worth discussing in it. The ultimate goal is to write the most suitable singleton code according to the actual needs.
Characteristics of the singleton pattern
The singleton pattern is designed to ensure that there is only one instance of a class and to provide a global access point to that instance. Then at least have the following characteristics:
- Cannot be initialized by other objects (constructor needs to be private)
- There is only one instance globally (you can only create one instance yourself)
- Provide unified access externally (provide static methods for external access to unique instances)
With these characteristics in mind, let’s take a look at the common singleton implementations.
Ps: Leave serialized tranportation and reflection to create a singleton for now
Several common implementations
One, hungry
The characteristics of
- Thread safety
- The lazy loading
Implementation idea:
- First, privatize the constructor to ensure that objects cannot be created by objects of other classes,
- And then new an object, a private static property inside INSTANCE,
- Provide a static method for external access
Code implementation:
Principles of thread safety and non-lazy loading
The code is simple to implement, but there is a big problem with this implementation: whether we use the singleton or not, we will initialize the singleton class and create the singleton object
Because we have a static property INSTANCE in our code, it is initialized when the JVM loads the class information. That is, new Sington() is executed to create a Sington object. That is, when the program starts loading the class information, the INSTANCE of the object is created. It is also thread-safe because objects are initialized during class loading.
The static section of the class load phase is thread-safe. We do not lock the static section.
This is due to the protection mechanism of the JVM loading process. Unlike normal classes where instances are allocated to the Java heap, static properties and methods of a class are stored in the static section of the method area, and are created during class loading, so they are affected by class loading.
The loading process of a class can be roughly divided into seven parts: loading, verification, preparation, parsing, initialization, use and unloading.
The order of the load, validate, prepare, initialize, and unload phases is determined, but not necessarily the parse phase, which in some cases can start after the initialization phase in order to support Java runtime binding.
About initialization: The JVM explicitly states that there are five and only five cases in which classes must be initialized (the load, validation, and preparation phases must occur before that)
- New, getstatic, putstatic, invokestatic, if the class is not initialized, it must be initialized. These instructions respectively refer to: new new object, read static variable, set static variable, call static function.
- When you make a reflection call to a class using the methods of the java.lang.Reflect package, you need to initialize the class if it is not already initialized
- When initializing a class, if the parent class is not initialized, the initialization of the parent class must be triggered first.
- When the VIRTUAL machine starts, the user needs to specify a main class (the class containing the main function) for execution. The virtual machine initializes this class first.
- However, with dynamic language support in JDK1.7, if an instance of MethodHandle resolves to REF_getStatic, REF_putStatic, or Ref_invokeStatic method handles, If the class to which the method handle corresponds is not initialized, initialization is triggered first.
Let’s look at the specific stages:
Loading stage
There are three main things to do in the loading phase:
- Get the binary stream of a class by its fully qualified name
- Transform the static storage structure represented by this byte stream into the runtime data structure of the method area
- Generate a java.lang.Class object in memory that represents the Class and serves as the method area for the Class’s various data access points.
Validation phase
The purpose of this phase is to ensure that the information contained in the Class file byte stream meets the requirements of the current VIRTUAL machine and does not compromise the security of the virtual machine.
Preparation stage
The preparation phase is when you formally allocate memory for class variables and set initial values for class static variables. The memory used by these variables is allocated in the method area.
First of all, allocating memory at this point only refers to static variables, not instance variables. Instance variables are allocated in heap memory along with the object when it is instantiated.
It is important to note that the initial value, usually the default value, is not the specified value. Such as:
public static int key = 123;
Copy the code
After the preparation phase, the key value is 0, not 123, because no Java methods have been executed, and the 123 key is assigned after the program is compiled and stored in the class constructor
() method. (During initialization)
Parsing stage
The parsing phase is the process of replacing symbolic references to the constant pool in the virtual machine with direct references.
Initialization phase
Class initialization stage is the last step of class loading. In the previous class loading process, except for the user can participate in the loading stage through the custom class loader, the rest is dominated and controlled by the JVM VIRTUAL machine. In the initialization stage, the Java code defined in the class is really executed.
In the preparation phase, static variables are assigned to the default values required by the system, while in the initialization phase, static variables are assigned to the values defined in our Java code.
This process (the initialization phase) executes the class constructor<clinit>()
Method, that is, assignment of static variables occurs at<clinit>()
Within the
The < Clinit >() method is generated by the compiler automatically collecting the assignment action of all static variables in the class and combining the statements in the static statement block in the order in which the statements appear in the source file. Only variables defined before the static block can be accessed. Variables defined after it can be assigned, but not accessed, as follows:
public class Test{
static{
i=0;// Assign a value to a variable, which can be compiled
System.out.print(i);// The compiler prompts: "Illegal forward reference"
}
static int i=1;
}
Copy the code
During the execution of the < Clinit >() method, the JVM locks the class, ensuring that only one thread can successfully execute the < Clinit >() method in a multi-threaded environment, all other threads are congested, and the < Clinit >() method can only be executed once. The congested thread is woken up and no longer executes the
() method. Unlike the class constructor (or instance constructor
() method), the
() method does not need to explicitly call the parent class constructor, and the virtual machine ensures that the
() of the parent class has been executed before the
() method of the subclass is executed.
Thus, multithreaded security for static code is implemented by locking it during class loading by the JVM.
Actually here also explains why not allowing static attributes of the class to use a static property, because the static properties of initialization is when creating objects CaiFu value, and the static attributes in the initialization phase of class loading has been assigned, and loaded into memory, the object of the class is created in the class loading after initialization phase, Of course you can’t assign.
Two, slacker style
The most important feature of lazy loading is that it can be initialized when we use it. Since it is initialized when we use it and not when the JVM class is loaded, there is no thread safety mechanism for the JVM to lock. The following are implemented from thread-unsafe to thread-safe.
Writing a
GetInstance () returns the INSTANCE static variable if it is not null, but null is a problem.
If INSTANCE = new Sington1() is executed by two threads simultaneously in the if statement, then two Sington1 objects may be created, which is obviously not what we want, so this is not thread safe.
Since threads are unsafe, does using the synchronized keyword solve the problem?
Now let’s look at notation 2:
Write two
Or write:
So these are essentially the same, they’re essentially locking the class.
Now that the class is locked, it is safe in multi-threaded situations and lazy loading is also implemented.
So is this perfect?
The answer is no, because there’s a problem with this: Synchronized is expensive, and every time we call the getInstance () method, we enter a block of synchronized wrapped code, even if the singleton has already been generated. It is not reasonable to enter synchronized without creating objects.
So to solve this problem, we have the following notation 3:
Write three
Synchronized (if (null == INSTANCE) {}) synchronized (if (null == INSTANCE) {}) synchronized (getInstance () {});
1. The singleton object is not generated
If (null == INSTANCE) {}, only one thread can create an object, so that once an object is generated, If (null == INSTANCE) is false, objects will not be created even if the next thread enters a synchronized block.
So it’s guaranteed to be unique.
2. A singleton object has been generated
If the singleton has already been generated, then the synchronized code block is not executed and the singleton is returned directly.
Here, too, is an improvement over the synchronized code block, which does not have to be entered every time the getInstance() method is called, improving performance.
Writing method 3 is also the common writing method of Double Check Locking (DCL).
Script 3 looks like lazy loading and thread-safe, but there is a problem that does not take into account the JVM compiler’s instruction reordering. We use script 4 to improve.
Write four
First look at the instruction reorder problem, after analysis to see the implementation code:
JVM compilation instructions are reordered
As the program runs, the compiler and processor reorder the specified order. However, the JMM (Java Memory Model) provides consistent Memory visibility for the upper layer by inserting specific types of Memory barriers to prevent specific types of compiler reordering and processor reordering across different compilers and different processor platforms.
Instruction reordering can change the order of code execution without affecting the result of code in a single thread, but the result of concurrent execution in multiple threads is not controllable.
For example, thread A:
context = getContext();
inited = true
Copy the code
Thread B:
while(! inited){ sleep(); } doSomeThingWithContext(context);Copy the code
Normally, if no reordering occurs, the code will execute fine, thread A will read inited true after getContext () assigns to the context, and thread B will break out of the loop when it reads inited true, There’s nothing wrong with doing doSomeThingWithContext.
But if instruction reordering occurs in A, the execution is different:
Suppose thread A has been reordered to:
inited = true
context = getContext();
Copy the code
Inited is true, and if thread B gets the execution right at that point, it breaks out of the while loop and doSomeThingWithContext(context); , but A’s context = getContext(); Thread B will get an empty context before the initialization is complete, causing uncontrollable errors.
This is an example we made up, just to understand the instruction reorder, let’s look at the three possible problems.
Corresponding to the problem in notation 3
The above instruction reorder problem corresponds to this line of code in writing method 3:
INSTANCE = new Sington1();
Copy the code
When executing this line of code, the JVM actually does three steps:
- Allocate memory to INSTANCE
- Call the constructor of Sington1 to initialize the variable
- INSTANCE refers to the allocated memory space, so it is not NULL
In combination with JVM instruction reordering, the possible order of execution is 1 -> 2 -> 3 and 1 -> 3 -> 2
If it is the first, there is no problem with execution.
If it’s the second, you might have A problem, if you have two threads A and B.
Thread A completes step 3, and thread B gets the execution right before step 2. At this point, INSTANCE is non-null, but no object is initialized, so thread B returns INSTANCE. At this point, there is no concrete object, so the next time you call the method in the singleton, you will get an error.
Volatile disables instruction reordering. Volatile disables instruction reordering. Volatile disables instruction reordering.
So now we have the best way to write slob, which is method four.
As you can see from the above analysis, lazy loading and thread safety are implemented by ourselves through locking and the volatile keyword. Is there any JVM that can implement lazy loading and thread safety?
The answer is yes, which is the following static inner class notation.
Static inner class
implementation
This implementation also takes advantage of Java’s classloading mechanism.
First, the JVM only loads the Sington2 class and does not execute the static methods in it, nor does it load the static inner class Sington2Holder. So you don’t create a singleton the first time the class is loaded.
When we use getInstance (), we use the static property Sington2Holder, which is used to load the static inner class Sington2Holder. Creates a singleton object and assigns it to the INSTANCE property. Again, these operations occur during class loading, are thread-safe by the JVM, and are loaded at the point of use, enabling lazy loading as well.
I personally prefer this approach to singletons, but there is a drawback to this approach: there is no way to pass values to the singleton class at initialization. At this point you can use the lazy four notation above to implement the singleton.
Fourth, the enumeration
You can see how simple it is to implement singletons with enumerations, and how simple it is to use:
// Get the singleton
Sington3.INSTANCE
// If the enumeration class has a method getString(), it can be called like this
Sington3.INSTANCE.getString()
Copy the code
Look at the pros and cons:
Advantages:
- Can guarantee the singleton in serialization transmission problems (implementation of serialization interface)
- Ensure that singletons are not reflected to create objects (JVM level disallows reflection)
- Thread safety
- Write simply
Disadvantages:
- No lazy loading
- The runtime footprint is much larger than that of non-enumerations
conclusion
The above respectively introduced hunhan-type, lazy, static inner class, enumeration of four singleton implementation methods, in the summary of the characteristics
Singleton implementation | Amount of code | Thread safety | Lazy loading | Can serialization transport ensure object uniqueness | Can reflection create object |
---|---|---|---|---|---|
The hungry type | less | is | no | no | is |
LanHanShi | more | is | is | no | is |
Static inner class | more | is | is | no | is |
The enumeration | Very few | is | no | no | no |
In fact, about the serialization transfer in Android development is basically not used (maybe I have not touched), about reflection to create objects is also an unconventional method, also not encountered, really should consider this situation, also can be implemented through code to serialization transfer and reflection to create objects.
Most we consideration is actually developing thread safety and lazy loading, is personally, I prefer in the form of a static inner class, but still want to choose according to actual business code, such as I need to create a singleton accept parameters, and also requires a singleton lazily, a static inner class is not appropriate, using LanHanShi would be better.
As for enumeration singleton implementation, I think Android is still less use enumeration, first can not be lazy load, secondly take up large memory, it seems that there is no other way to have a sense of security, why.
Welcome to follow my official account: