Correct Implementation of Double Check Lock (double checked locking) in Java

  • 2021-11-13 07:27:49
  • OfStack

Directory preface locking Double check lock Error double-checked lock
Hidden danger
Correct double-checked lock
Summarize

Preface

When implementing the singleton pattern, if multithreading is not considered, it is easy to write the following error code:


public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            uniqueSingleton = new Singleton();
        }
        return uniqueSingleton;
    }
}

In the case of multithreading, writing like this may result in multiple instances of uniqueSingleton. Consider two threads calling getInstance () at the same time, for example:

Time Thread A Thread B
T1 检查到uniqueSingleton为空
T2 检查到uniqueSingleton为空
T3 初始化对象A
T4 返回对象A
T5 初始化对象B
T6 返回对象B

As you can see, uniqueSingleton is instantiated twice and held by different objects. Completely contrary to the original intention of singleton.

Lock

When this happens, the first reaction is to lock it, as follows:


public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public synchronized Singleton getInstance() {
        if (null == uniqueSingleton) {
            uniqueSingleton = new Singleton();
        }
        return uniqueSingleton;
    }
}

Although this solves the problem, because synchronized is used, it will lead to a lot of performance overhead, and locking only needs to be used at the first initialization, and there is no need to lock later calls.

Double check lock

Double check lock (double checked locking) is an optimization to the above problems. Determine whether the object has been initialized before deciding whether to lock it.

Error double-checked lock


public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();   // error
                }
            }
        }
        return uniqueSingleton;
    }
}

If you write this, the running sequence becomes:

Check whether the variable is initialized (without obtaining the lock), and return immediately if it is initialized. Gets the lock. Check again whether the variable has been initialized, and initialize 1 object if it has not been initialized.

Double checking is performed because if multiple threads pass the first check at the same time, and one of them first passes the second check and instantiates the object, the remaining threads that pass the first check will not instantiate the object again.

In this way, all subsequent calls will avoid locking and return directly, except for locking during initialization, thus solving the problem of performance consumption.

Hidden danger

The above writing seems to solve the problem, but there is a big hidden danger. The line of code that instantiates the object (the line labeled error) can actually be broken down into the following three steps:

Allocate memory space Initialize object Point the object to the memory space just allocated

However, some compilers may reorder steps 2 and 3 for performance reasons, and the order is as follows:

Allocate memory space Point the object to the memory space just allocated Initialize object

Now considering the reordering, the two threads make the following calls:

Time Thread A Thread B
T1 检查到uniqueSingleton为空
T2 获取锁
T3 再次检查到uniqueSingleton为空
T4 为uniqueSingleton分配内存空间
T5 将uniqueSingleton指向内存空间
T6 检查到uniqueSingleton不为空
T7 访问uniqueSingleton(此时对象还未完成初始化)
T8 初始化uniqueSingleton

In this case, the thread B accesses uniqueSingleton at time T7, and accesses an object whose initialization is not completed.

Correct double-checked lock


public class Singleton {
    private volatile static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();
                }
            }
        }
        return uniqueSingleton;
    }
}

In order to solve the above problems, we need to add the keyword volatile before uniqueSingleton. With the volatile keyword, reordering is disabled and all write (write) operations occur before read (read) operations.

At this point, the double-checked lock can work perfectly.

Summarize

References:

Double check locking mode How to implement a single case with double check lock in Java: http://www. importnew. com/12196. html Double-check locking and delayed initialization

Related articles: