【Design Pattern】Concurrency - Double-checked Locking Pattern

Posted by 西维蜀黍 on 2021-06-27, Last Modified on 2022-12-10

Introduction

This pattern reduces the number of lock acquisitions by simply checking the locking condition beforehand. As a result of this, there’s usually a performance boost.

Consider, for example, this code segment in the Java programming language as given (as well as all other Java code segments):

// Single-threaded version
class Foo {
    private static Helper helper;
    public Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }

    // other functions and members...
}

The problem is that this does not work when using multiple threads. A lock must be obtained in case two threads call getHelper() simultaneously. Otherwise, either they may both try to create the object at the same time, or one may wind up getting a reference to an incompletely initialized object.

To begin with, let’s consider a simple singleton with draconian synchronization:

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

    // private constructor and other methods ...
}

Despite this class being thread-safe, we can see that there’s a clear performance drawback: each time we want to get the instance of our singleton, we need to acquire a potentially unnecessary lock.

Solution

To fix that, we could instead start by verifying if we need to create the object in the first place and only in that case we would acquire the lock.

Going further, we want to perform the same check again as soon as we enter the synchronized block, in order to keep the operation atomic:

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

    // private constructor and other methods...
}

One thing to keep in mind with this pattern is that the field needs to be *volatile* to prevent cache incoherence issues. In fact, the Java memory model allows the publication of partially initialized objects and this may lead in turn to subtle bugs.

Solution in Golang

package main

import "sync"

var arrOnce sync.Once
var arr []int

// getArr retrieves arr, lazily initializing on first call. Double-checked
// locking is implemented with the sync.Once library function. The first
// goroutine to win the race to call Do() will initialize the array, while
// others will block until Do() has completed. After Do has run, only a
// single atomic comparison will be required to get the array.
func getArr() []int {
	arrOnce.Do(func() {
		arr = []int{0, 1, 2}
	})
	return arr
}

func main() {
	// thanks to double-checked locking, two goroutines attempting to getArr()
	// will not cause double-initialization
	go getArr()
	go getArr()
}

Reference