Double-checked locking

From Wikipedia, the free encyclopedia

In software engineering, double-checked locking is a software design pattern originally known as "double-checked locking optimization." The pattern is unsafe in some versions of some languages on some modern computer hardware and/or optimizing compilers. It can therefore sometimes be considered to be an anti-pattern.

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.

Contents

[edit] Usage in Java

Consider, for example, this code segment in the Java programming language as given by [1]:

// 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 instance 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 instance 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:

  1. Checking that the variable is initialized (without obtaining the lock). If it is initialized, it is returned immediately.
  2. Obtaining the lock.
  3. 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.
  4. Otherwise, the variable is initialized and returned.

'Broken' example code segment in Java as given by [2]:

// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      synchronized(this) {
        if (helper == null) {
          helper = new Helper();
        }    
      }    
    }    
    return helper;
  }
  // other functions and members...
}

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:

  1. Thread A notices that the value is not initialized, so it obtains the lock and begins to initialize the value.
  2. 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.
  3. 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 in J2SE 1.4 (and earlier versions) 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 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.

As of J2SE 5.0, this problem has been fixed. The volatile keyword now ensures that multiple threads handle the singleton instance correctly. This new idiom is described in [3]:

 // Works with acquire/release semantics for volatile
 // Broken under Java 1.4 and earlier semantics for volatile
 class Foo {
   private volatile Helper helper = null;
   public Helper getHelper() {
     if (helper == null) {
       synchronized(this) {
         if (helper == null)
           helper = new Helper();
       }
     }
     return helper;
   }
 }
  1. Correct locking example code segment in Java as given by [4]:

For correct locking in J2SE 1.4 and earlier, one possible solution is to synchronize the getHelper() method. But keep in mind that synchronizing a method can decrease performance by a factor of 100 or higher. So if getHelper() is called frequently in the application, this solution should be avoided, especially if the Helper object is not "expensive" to create.

 // Correct multithreaded version for J2SE 1.4 and earlier
 class Foo { 
   private Helper helper = null;
   public synchronized Helper getHelper() {
     if (helper == null) 
         helper = new Helper();
     return helper;
   }
   // other functions and members...
 }

[edit] Usage in Microsoft Visual C++

Double-checked locking can be implemented in Visual C++ 2005 if the pointer to the resource is marked as being volatile. Visual C++ 2005 guarantees that volatile variables will behave as fences, as in J2SE 5.0, preventing both compiler and CPU arrangement of reads and writes. There is no such guarantee in previous versions of Visual C++. However, marking the pointer to the resource as volatile may harm performance elsewhere, if the pointer declaration is visible elsewhere in code, by forcing the compiler to treat it as a fence elsewhere, even when it is not necessary.

[edit] See also

The Test and Test-and-set idiom for a low-level locking mechanism.

[edit] External links

In other languages