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++ ==