Double-checked locking
|
Double-checked locking is a software design pattern originally known as "double-checked locking optimization." The pattern is usually unsafe on modern computer hardware and/or optimizing compilers.
It is typically used to reduce locking overhead when implementing "lazy initialization" in a multi-threaded environment. Lazy initialization avoids initializing a value until the first time it is accessed. Considering for example this code segment in Java as given by [1] (http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html):
// Single threaded version class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) helper = new Helper(); return helper; } // other functions and members... }
However, when using threads a lock must be obtained in case two threads attempt to initialize the helper variable simultaneously. Intuitively, the overhead of acquiring and releasing a lock every time this method is called seems unnecessary: the main use of the lock appears to be to ensure that the initialization of the variable will only be done once, but once the initialization has been completed, acquiring and releasing the locks would appear unnecessary. Many programmers have attempted to optimize this situation in the following manner:
- Checking that the variable is initialized (without obtaining the lock). If it is initialized, it is returned immediately.
- Obtaining the lock.
- Double-checking whether the variable has already been initialized: if another thread acquired the lock first, it may have already done the initialization. If so, the initialized variable is returned.
- Otherwise, the variable is initialized and returned.
Intuitively, this algorithm seems like an efficient solution to the problem. However, this technique has many subtle problems and should usually be avoided. 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 most 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.
- 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 the variable is used before A finishes initializing it, the program will likely crash.
One of the dangers of using double-checked locking is that it will often appear to work: it is not easy to distinguish between a correct implementation of the technique and one that has subtle problems. Depending on the hardware platform, 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-checking locking may only occur intermittently. Reproducing the failures can be difficult.
Double-checked locking is an example of an anti-pattern. See also Test and Test-and-set idiom.
Reference
- The "Double-Checked Locking is Broken" Declaration (http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html); David Bacon et al.
- Double-checked locking and the Singleton pattern (http://www-106.ibm.com/developerworks/java/library/j-dcl.html)