MarketDouble-checked locking
Company Profile

Double-checked locking

In software engineering, double-checked locking is a software design pattern used to reduce the overhead of acquiring a lock by testing the locking criterion before acquiring the lock. Locking occurs only if the locking criterion check indicates that locking is required.

Motivation and original pattern
Consider, for example, this code segment in the Java programming language: the overhead of acquiring and releasing a lock every time this method is called after the initialization has been completed seems unnecessary. Many programmers, including the authors of the double-checked locking design pattern, have attempted to optimize this situation in the following manner: • Check that the variable is initialized (without obtaining the lock). If it is initialized, return it immediately. • Obtain the lock. • Double-check whether the variable has already been initialized: if another thread acquired the lock first, it may have already done the initialization. If so, return the initialized variable. • Otherwise, initialize and return the variable. // Broken multithreaded version // original "Double-Checked Locking" idiom class Foo { private Helper helper; public Helper getHelper() { if (helper == null) { synchronized (this) { if (helper == null) { helper = new Helper(); } } } return helper; } // other functions and members... } Intuitively, this algorithm is an efficient solution to the problem. But if the pattern is not written carefully, it will have a data race. For example, consider the following sequence of events: • Thread A notices that the value is not initialized, so it obtains the lock and begins to initialize the value. • Due to the semantics of some programming languages, the code generated by the compiler is allowed to update the shared variable to point to a partially constructed object before A has finished performing the initialization. For example, in Java if a call to a constructor has been inlined then the shared variable may immediately be updated once the storage has been allocated but before the inlined constructor initializes the object. • Thread B notices that the shared variable has been initialized (or so it appears), and returns its value. Because thread B believes the value is already initialized, it does not acquire the lock. If B uses the object before all of the initialization done by A is seen by B (either because A has not finished initializing it or because some of the initialized values in the object have not yet percolated to the memory B uses (cache coherence)), the program will likely crash. Most runtimes have memory barriers or other methods for managing memory visibility across execution units. Without a detailed understanding of the language's behavior in this area, the algorithm is difficult to implement correctly. One of the dangers of using double-checked locking is that even a naive implementation will appear to work most of the time: it is not easy to distinguish between a correct implementation of the technique and one that has subtle problems. Depending on the compiler, the interleaving of threads by the scheduler and the nature of other concurrent system activity, failures resulting from an incorrect implementation of double-checked locking may only occur intermittently. Reproducing the failures can be difficult. == Usage in C++ ==
Usage in C++
For the singleton pattern, double-checked locking is not needed: Singleton& getInstance() { static Singleton s; return s; } C++11 and beyond also provide a built-in double-checked locking pattern in the form of std::once_flag and std::call_once: import std; using std::once_flag; using std::optional; class Singleton { private: Singleton() = default; static optional instance; static once_flag flag; public: static Singleton* getInstance() { std::call_once( Singleton::flag, []() -> void { instance.emplace(Singleton()); } ); return &instance; } }; If one wishes to manually implement an explicit and proper double-checked idiom instead of the trivially working example above (for instance because Visual Studio before the 2015 release did not implement the C++11 standard's language about concurrent initialization quoted above), one needs at least atomic operations with acquire/release memory ordering as in this code (explicit memory fence statements also work but may use slower CPU instructions e.g. on ARM64): import std; using std::atomic; using std::lock_guard; using std::mutex; class Singleton { private: Singleton() = default; static atomic instance; static mutex m; public: static Singleton* getInstance() { Singleton* p = instance.load(std::memory_order_acquire); if (!p) { // 1st check lock_guard lock(m); p = instance.load(std::memory_order_relaxed); if (!p) { // 2nd (double) check p = new Singleton(); instance.store(p, std::memory_order_release); } } return p; } ~Singleton() { // cleanup logic } }; == Usage in POSIX ==
Usage in POSIX
pthread_once() must be used to initialize library (or sub-module) code when its API does not have a dedicated initialization procedure required to be called in single-threaded mode. == Usage in Go ==
Usage in Go
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() } == Usage in Java ==
Usage in Java
As of J2SE 5.0, the volatile keyword is defined to create a memory barrier. This allows a solution that ensures that multiple threads handle the singleton instance correctly. This new idiom is described in and [http://www.oracle.com/technetwork/articles/javase/bloch-effective-08-qa-140880.html. // Works with acquire/release semantics for volatile in Java 1.5 and later // Broken under Java 1.4 and earlier semantics for volatile class Foo { private volatile Helper helper; public Helper getHelper() { Helper localRef = helper; if (localRef == null) { synchronized (this) { localRef = helper; if (localRef == null) { helper = localRef = new Helper(); } } } return localRef; } // other functions and members... } Note the local variable "", which seems unnecessary. The effect of this is that in cases where is already initialized (i.e., most of the time), the volatile field is only accessed once (due to "" instead of ""), which can improve the method's overall performance by as much as 40 percent. Java 9 introduced the class, which allows use of relaxed atomics to access fields, giving somewhat faster reads on machines with weak memory models, at the cost of more difficult mechanics and loss of sequential consistency (field accesses no longer participate in the synchronization order, the global order of accesses to volatile fields). import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; // Works with acquire/release semantics for VarHandles introduced in Java 9 class Foo { private volatile Helper helper; private static final VarHandle HELPER; public Helper getHelper() { Helper localRef = getHelperAcquire(); if (localRef == null) { synchronized (this) { localRef = getHelperAcquire(); if (localRef == null) { localRef = new Helper(); setHelperRelease(localRef); } } } return localRef; } private Helper getHelperAcquire() { return (Helper) HELPER.getAcquire(this); } private void setHelperRelease(Helper value) { HELPER.setRelease(this, value); } static { try { MethodHandles.Lookup lookup = MethodHandles.lookup(); HELPER = lookup.findVarHandle(Foo.class, "helper", Helper.class); } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); } } // other functions and members... } If the helper object is static (one per class loader), an alternative is the initialization-on-demand holder idiom (See Listing 16.6 from the previously cited text.) // Correct lazy initialization in Java class Foo { private static class HelperHolder { public static final Helper helper = new Helper(); } public static Helper getHelper() { return HelperHolder.helper; } } This relies on the fact that nested classes are not loaded until they are referenced. Semantics of field in Java 5 can be employed to safely publish the helper object without using : public class FinalWrapper { public final T value; public FinalWrapper(T value) { this.value = value; } } public class Foo { private FinalWrapper helperWrapper; public Helper getHelper() { FinalWrapper tempWrapper = helperWrapper; if (tempWrapper == null) { synchronized (this) { if (helperWrapper == null) { helperWrapper = new FinalWrapper(new Helper()); } tempWrapper = helperWrapper; } } return tempWrapper.value; } } The local variable is required for correctness: simply using for both null checks and the return statement could fail due to read reordering allowed under the Java Memory Model. Performance of this implementation is not necessarily better than the implementation. == Usage in C# ==
Usage in C#
In .NET Framework 4.0, the Lazy<T> class was introduced, which internally uses double-checked locking by default (LazyThreadSafetyMode.ExecutionAndPublication mode) to store either the exception that was thrown during construction, or the result of the function that was passed to Lazy<T>: using System; public class MySingleton { private static readonly Lazy _mySingleton = new Lazy(() => new MySingleton()); private MySingleton() { } public static MySingleton Instance => _mySingleton.Value; } == See also ==
tickerdossier.comtickerdossier.substack.com