Delayed initialization of java multithreading

Delayed initialization of java multithreading

Sometimes we may postpone the initialization of some expensive objects, and initialize them only when these objects are used. Developers can use lazy initialization to achieve this requirement. But to correctly implement thread-safe lazy initialization still requires some skills, otherwise it is prone to problems. The following is an example of non-thread-safe lazy initialization:

public class UnsafeLazyInit {
	
	private static Instance instance;
	
	public static Instance getInstance() {
		if(instance == null) {          //1:A 
			instance = new Instance();  //2:B 
		}
		return instance;
	}

} 
In the UnsafeLazyInit class, suppose that thread A executes code 1 while thread B executes code 2. At this time, thread A may see that the instance object has not yet been initialized.
For the UnsafeLazyInit class, we can synchronize the getInstance method to achieve thread-safe delayed initialization. The code is as follows:

public class UnsafeLazyInit {
	
	private static Instance instance;
	
	public synchronized static Instance getInstance() {
		if(instance == null) {           //1:A 
			instance = new Instance();   //2:B 
		}
		return instance;
	}

} 
Because the getInstance method is synchronized, it will cause performance overhead. If the getInstance method is frequently called by multiple threads, it will cause the performance of the program to decrease, and if getInstance is not called frequently by multiple threads, then this solution will Will provide satisfactory performance.
For the decrease in program execution performance that the synchronized method may bring, we can use a "smart" technique: Double-Checked Locking to reduce synchronization overhead. The following is an example code that uses double-checked locks to implement delayed initialization:
public class DoubleCheckedLocking {

	private static Instance instance;
	
	public static Instance getInstance() {
		if(instance == null) {                             //1: 
			synchronized(DoubleCheckedLocking.class) {     //2: 
				if(instance == null) {                     //3: 
					instance = new Instance();             //4: 
				}
			}
		}
		return instance;
	}

} 
According to the above code, if the first check instance is not null, you do not need to perform the following locking and secondary checking and initialization operations, so the performance overhead caused by synchronization can be greatly reduced, which seems to be the best of both worlds.
Double-checked locking may seem perfect, but it is a wrong optimization! When the thread executes 1: The first check, the code reads that the instance is not null. In fact, the instance has not been initialized yet. The root of the problem lies in the reordering.
When creating an instance instance, the line of instance = new Instance() can be decomposed into the following 3 lines of pseudo code:

memory = allocate();   //1 
ctorInstance(memory);  //2:  
instance = memory;     //3:  instance  

Between 2 and 3 in the above pseudo code, reordering may occur. The execution order after reordering is as follows:

memory = allocate();  //1 
instance = memory;    //3:  instance    
ctorInstance(memory); //2:   

In the above java code, if instance = new Instance() is reordered, another concurrent thread B may have instance not null at the first check, and thread B will then access the object referenced by instance. But at this time the object may not have been initialized by thread A, that is, an uninitialized object will be accessed.
After knowing the source of this problem, there are two ways to implement thread-safe delayed initialization:
1. Reordering 2 and 3 is not allowed.
2. Allow 2 and 3 reordering, but not allow other threads to "see" this reordering.

1. Reordering 2 and 3 are not allowed, only a small modification to double check lock is enough. We declare instance as volatile to realize thread-safe delayed initialization. The sample code is as follows:

public class DoubleCheckedLocking {

	private volatile static Instance instance;
	
	public static Instance getInstance() {
		if(instance == null) {
			synchronized(DoubleCheckedLocking.class) {
				if(instance == null) {
					instance = new Instance();  //instance volatile 
				}
			}
		}
		return instance;
	}

} 
When the object is declared as volatile, the reordering of 2 and 3 in the pseudo code will be prohibited in a multi-threaded environment.

2. Allow 2 and 3 reordering, but not allow other threads to "see" this reordering.
JVM in the initialization phase of the class (that is, after the Class is loaded and before being used by the thread), it will perform the initialization of the class. During the initialization of the class, the JVM will acquire a lock, which can synchronize the initialization of a class by multiple threads. Based on this feature, we can implement thread-safe lazy initialization while allowing 2 and 3 reordering.

public class InstanceFactory {
	
	private static class InstanceHolder {
		public static Instance instance = new Instance();
	}
	
	public static Instance getInstance() {
		return InstanceHolder.instance;   //InstanceHolder 
	}
	
} 
The essence of this scheme is: 2 and 3 reordering are allowed, but non-structural threads (such as thread B) are not allowed to "see" this reordering.
In InstanceFactory, the thread that executes the getInstance method for the first time (such as thread A) will cause the InstanceHolder class to be initialized, but what if multiple threads call the getInstance method at the same time?
The Java language specification stipulates that for each class or interface C, there is a unique initialization lock LC corresponding to it. The mapping from C to LC is freely implemented by the specific implementation of the JVM. The JVM acquires this initialization lock during initialization, and each thread acquires the lock at least once to ensure that this class is initialized.
This process is relatively lengthy, and I will not describe it here. In short, the JVM synchronizes the operation of multiple threads to initialize an object at the same time through the initialization lock to ensure that the class will not be initialized multiple times.

By comparing the volatile-based double check locking scheme and the class-based initialization scheme, we find that the class-based initialization scheme is more concise. However, the volatile-based double-checked locking scheme has an additional advantage: in addition to lazy initialization of static fields, lazy initialization of instance fields can also be achieved.

In the design mode, there is a singleton mode (Singleton), which is more commonly used. We can use volatile-based double-checked locking and class-based initialization schemes to create singleton objects. In actual work, I generally use The scheme of class initialization is to implement the singleton pattern.