Concurrency error caused by delayed initialization

Posted by uptime on Sat, 22 Jan 2022 22:20:41 +0100

Q: Why does delayed initialization occur

1. In the following code, if multiple threads compete for the lock, it will have a great overhead

public class SafeLazyInitialization {
    private static Instance instance;
    public synchronized static Instance getInstance() {
        if (instance == null)
            instance = new Instance();
        return instance;
    }
}

In order to reduce the performance loss caused by locks, double check locking is used, as shown in the following code

public class DoubleCheckedLocking {                 
    private static Instance instance;                    
    public static Instance getInstance() {               
        if (instance == null) {                       //4: First inspection
            synchronized (DoubleCheckedLocking.class) {  //5: Lock
                if (instance == null)                    //6: Second inspection
                    instance = new Instance();           //7: This is the root of the problem
            }                                           
        }                                                
        return instance;                              
    }                                                  
}                                                      

During the first check, another thread accesses that the instance variable is not empty and does not need to wait to obtain the lock, which greatly reduces the loss of the lock.

Q: What are the concurrent errors caused by delayed initialization?

But this is a wrong optimization. When another thread judges that instance is not empty when the program executes to line 4, the program has not completed initialization.
To initialize an object, you need to go through three steps:

instance = new Instance();  
This line of code can consist of three parts,
// 1. Allocate memory space
alloctMemory(instance);
// 2. Initialization object
initial(instance);
// 3. Set instance to point to the memory address just allocated
instance = memory

Because 2 and 3 will cause errors due to instruction reordering, this problem needs to be optimized.
As long as 2 is in front of 4, there will be no problem. The waiting of the first access object must have been initialized.

A: There are two solutions

Solution 1 Use volatile to prevent reordering

The solution (1) is to prohibit reordering by using volatile to modify variables

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;
    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();//instance is volatile, and there is no problem now
            }
        }
        return instance;
    }
}

Solution 2 Synchronization mechanism based on class initialization

Solution (2) run the thread reordering, but do not allow other threads to see the reordering process.
In the class initialization phase, the jvm will first load the class object into memory, and then use the jvm lock to synchronize the class initialization of multiple threads.

public class InstanceFactory {
    // Using static inner classes
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }
    
    // Calling methods of static inner classes
    public static Instance getInstance() {
        return InstanceHolder.instance ;  //This will cause the InstanceHolder class to be initialized
    }
}


In this way, the class initialization is locked directly, so thread B can't see the reordering
Then you can use this idea to write the double check code into the code for class initialization.

(extension) when will the class be initialized?

(detailed explanation) synchronization mechanism in class initialization

It is divided into four stages:

Phase 1 (contention to obtain initialization lock)

Synchronize on the class object, that is, obtain the initialization lock of the class object to initialize the class and interface. The thread that obtains the lock will wait until it obtains the lock.

Class A = classLoader.loadClass(InstanceHolder.class);

If threads A and B compete to initialize Class object A at the same time

The second stage (class initialization and static variable initialization)

When A obtains the initialization lock, A will initialize the class (if it is A static class, it will initialize the static class) and the static field, and B will obtain the initialization lock and check the state of the lock

Stage 3: thread A sets the state of class to initialized, wakes up all threads in the condition in the lock, and releases the persistent lock


Stage 4: thread B ends the class initialization process

Thread a initializes the class in the second stage A1, and thread B obtains the initialization lock only after releasing the lock in the third stage A4. There is a happy before relationship here

※ note 1: the condition and state tags here are fictitious. The specific implementation of JVM only needs to implement similar functions.

(description) usage scenario of solution 2

Usage scenario of solution 2: each time the class executes, the lock will be initialized first, and then the state state of the lock will be read. If the state of the lock is initialized, the lock will be released and the method will be called directly. If the class has been initialized, if two threads access the delayed initialization code at the same time, there will still be an error problem caused by reordering.
Therefore, using class initialization to solve the problems caused by delayed initialization is only applicable when the class has not been initialized.

reference resources: https://zhuanlan.zhihu.com/p/34169213

Topics: Java