Why do double check locks use volatile fields?

The origin of double lock

In the singleton mode, there is a DCL (double lock) implementation. In Java programs, sometimes it may be necessary to delay some expensive object initialization operations, and only start initialization when these objects are used.

Here is the instance code of the non thread safe delay initialization object.

/**
 * @author xiaoshu
 */
public class Instance {
}

/**
 * Non thread safe delay initialization object
 *
 * @author xiaoshu
 */
public class UnsafeLazyInitialization {
    private static Instance instance;

    public static Instance getInstance() {
        if (null == instance) {
            instance = new Instance();
        }
        return instance;
    }
}

In the UnsafeLazyInitialization class, assume that A thread executes code 1 while B thread executes code 2. At this point, thread A may see that the instance reference object has not completed initialization.

For the UnsafeLazyInitialization class, we can synchronize the getInstance() method to achieve thread safe delayed initialization. The sample code is as follows.

/**
 * Secure delay initialization
 *
 * @author xiaoshu
 */
public class SafeLazyInitialization {
    private static Instance instance;

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

synchronized results in performance overhead due to synchronization of the getInstance () method. If the getInstance () method is called frequently by multiple threads, the performance of the program will be reduced. On the contrary, if getInstance() method is not called frequently by multiple threads, this delay initialization scheme will provide satisfactory performance.

Later, a "smart" technique was proposed: double checked locking. We want to reduce the cost of synchronization by double checking locking. The following is the example code that uses double check locking to achieve delayed initialization.

/**
 * Double check lock
 *
 * @author xiaoshu
 */
public class DoubleCheckedLocking {
    private static Instance instance;

    public static Instance getInstance() {
        if (null == instance) {                             //1. First inspection
            synchronized (DoubleCheckedLocking.class) {     //2. lock up
                if (null == instance) {                     //3: second inspection
                    instance = new Instance();              //4. The root of the problem is here
                }
            }
        }
        return instance;
    }
}

Double check locking seems perfect, but it's a wrong optimization! When the thread executes to position 1 and the code reads that the instance is not null, the object referenced by the instance may not have completed initialization.

The root cause of the problem

The previous double check locks an object at point 4 of the instance code (instance = new Instance();). This line of code can be broken down into the following three lines of pseudo code.

memory = allocate();    //1. Allocate the memory space of the object
ctorInstance(memory); //2. Initialization object
instance = memory;        //3. Set the instance to point to the newly allocated memory address

Between 2 and 3 in the above three lines of pseudo code, it may be reordered (in some JIT compilers, this reordering actually happens). The execution sequence after reordering between 2 and 3 is as follows:

memory = allocate();    //1. Allocate the memory space of the object
instance = memory;        //3. Set the instance to point to the newly allocated memory address
                                            //Note that the object has not been initialized yet!
ctorInstance(memory); //2. Initialization object

Multithreaded execution schedule

time Thread A Thread B
T1 A1: allocate the memory space of the object
T2 A3: set instance to point to memory space
T3 B1: judge whether the instance is empty
T4 B2: because instance is not null, thread B will access the object referenced by instance
T5 A2: initialization object
T6 A4: access the object referenced by instance

After knowing the root cause of the problem, we can come up with two ways to achieve thread safe delayed initialization.

1) 2 and 3 reordering is not allowed

2) allow 2 and 3 reordering, but do not allow other threads to "see" the reordering.

The two solutions introduced later correspond to the above two points.

Solution 1: a volatile based solution

/**
 * Safe double check lock
 *
 * @author xiaoshu
 */
public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

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

Note: this solution requires JDK5 or later (because the new JSR-133 memory model specification, which enhances the semantics of volatile, has been used since JDK5).

When the reference of declared object is volatile, the reordering between 2 and 3 in 3 lines of pseudo code will be prohibited in multi-threaded environment.

Solution 2: solution based on class initialization

The JVM performs Class initialization during the Class initialization phase (that is, after Class is loaded and before it is used by threads). During Class initialization, the JVM will acquire a lock that can synchronize the initialization of the same Class by multiple threads.

Based on this feature, another thread safe delay initialization scheme (called initialization on demand holder idom) can be implemented.

/**
 * Solution based on class initialization
 *
 * @author xiaoshu
 */
public class InstanceFactory {
    private static class InstanceHolder {
        private static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance; //This will cause the InstanceHolder class to be initialized
    }
}

Delayed field initialization reduces the cost of initializing a class or creating an instance, but increases the cost of accessing the delayed field. In most cases, normal initialization is better than delayed initialization. If you really need to use thread safe delay initialization for instance fields, please use the scheme based on volatile described above; if you really need to use thread safe delay initialization for static fields, please use the scheme based on class initialization described above.

Reference resources:

  1. The art of Java Concurrent Programming

Tags: Java jvm Programming

Posted on Sun, 10 Nov 2019 07:33:11 -0500 by wazo00