Double-checked locking

Double-checked locking

In software engineering, double-checked locking (also known as "double-checked locking optimization[1]".) is a software design pattern used to reduce the overhead of acquiring a lock by first testing the locking criterion (the "lock hint") without actually acquiring the lock. Only if the locking criterion check indicates that locking is required does the actual locking logic proceed.

The pattern, when implemented in some language/hardware combinations, can be unsafe. At times, it can be considered an anti-pattern.[citation needed]

It is typically used to reduce locking overhead when implementing "lazy initialization" in a multi-threaded environment, especially as part of the Singleton pattern. Lazy initialization avoids initializing a value until the first time it is accessed.

Contents

Usage in Java

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

// Single threaded version
class Foo {
    private Helper helper = null;
    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.

The lock is obtained by expensive synchronizing, as is shown in the following example.

// Correct but possibly expensive multithreaded version
class Foo {
    private Helper helper = null;
    public synchronized Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }
 
    // other functions and members...
}

However, the first call to getHelper() will create the object and only the few threads trying to access it during that time need to be synchronized; after that all calls just get a reference to the member variable. Since synchronizing a method can decrease performance by a factor of 100 or higher,[2] the overhead of acquiring and releasing a lock every time this method is called seems unnecessary: 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. Check that the variable is initialized (without obtaining the lock). If it is initialized, return it immediately.
  2. Obtain the lock.
  3. 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.
  4. Otherwise, initialize and return the variable.
// 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 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.

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 [4]:

// 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() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }
 
    // other functions and members...
}

Note the usage of the local variable result which seems unnecessary. For some versions of the Java VM, it will make the code 25% faster and for others, it won't hurt.[3]

If the helper object is static (one per class loader), an alternative is the initialization on demand holder idiom [4] See Listing 16.6 on [5]

// Correct lazy initialization in Java 
@ThreadSafe
class Foo {
    private static class HelperHolder {
       public static Helper helper = new Helper();
    }
 
    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}

This relies on the fact that inner classes are not loaded until they are referenced.

Semantics of final field in Java 5 can be employed to safely publish the helper object without using volatile:[6]

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) { 
        this.value = value; 
    }
}
 
public class Foo {
   private FinalWrapper<Helper> helperWrapper = null;
 
   public Helper getHelper() {
      FinalWrapper<Helper> wrapper = helperWrapper;
 
      if (wrapper == null) {
          synchronized(this) {
              if (helperWrapper == null) {
                  helperWrapper = new FinalWrapper<Helper>(new Helper());
              }
              wrapper = helperWrapper;
          }
      }
      return wrapper.value;
   }
}

The local variable wrapper is required for correctness. Performance of this implementation is not necessarily better than the volatile implementation.

Usage in Microsoft Visual C++

Double-checked locking can be implemented in Visual C++ 2005 and above if the pointer to the resource is declared with the C++ keyword volatile. Visual C++ 2005 guarantees that volatile variables will behave as fence instructions, as in J2SE 5.0, preventing both compiler and CPU arrangement of reads and writes with acquire semantics (for reads) and release semantics (for writes).[7] 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.

Usage in Microsoft .NET (Visual Basic, C#)

Double-checked locking can be implemented efficiently in .NET with careful use of the MemoryBarrier instruction:

public class MySingleton {
    private static object myLock = new object();
    private static MySingleton mySingleton = null;
    private static bool ready = false;
 
    private MySingleton() { 
    }
 
    public static MySingleton GetInstance() {
        if (!ready) { // 1st check
            lock (myLock) {
                if (!ready) { // 2nd (double) check
                    mySingleton = new MySingleton();
                    System.Threading.Thread.MemoryBarrier();    // fence
                    ready = true;
                }
            }
        }
        return mySingleton;
    }
}

In this example, the "lock hint" is the ready flag which can only change after mySingleton is fully constructed and ready for use.

Alternatively, the C# keyword volatile can be used to enforce read/write fences around all access of mySingleton, which would negate many of the efficiencies inherent in the double-checked locking strategy.

public class MySingleton {
    private static object myLock = new object();
    private static volatile MySingleton mySingleton = null;
 
    private MySingleton() { 
    }
 
    public static MySingleton GetInstance() {
        if (mySingleton == null) { // check
            lock (myLock) {
                if (mySingleton == null) { // double check, volatile ensures that the value is re-read
                    mySingleton = new MySingleton();
                }
            }
        }
        return mySingleton;
    }
}

See also

References

  1. ^ Schmidt, D et al. Pattern-Oriented Software Architecture Vol 2, 2000 pp353-363
  2. ^ Boehm, Hans-J. "Threads Cannot Be Implemented As a Library", ACM 2005, p265
  3. ^ Joshua Bloch "Effective Java, Second Edition", p. 283
  4. ^ Brian Goetz et al. Java Concurrency in Practice, 2006 pp348
  5. ^ [1]
  6. ^ [2] Javamemorymodel-discussion mailing list
  7. ^ http://msdn.microsoft.com/en-us/library/12a04hfd(VS.100).aspx

External links


Wikimedia Foundation. 2010.

Игры ⚽ Нужно решить контрольную?

Look at other dictionaries:

  • Double checked locking — En génie logiciel, le verrouillage à double test ou double checked locking est un ancien patron de conception[1]. Considéré aujourd hui comme un antipattern du fait des problèmes subtils et difficiles à déceler qu il pose, il a été utilisé dans… …   Wikipédia en Français

  • Double-checked locking — En génie logiciel, le verrouillage à double test ou double checked locking est un ancien patron de conception[1]. Considéré aujourd hui comme un antipattern du fait des problèmes subtils et difficiles à déceler qu il pose, il a été utilisé dans… …   Wikipédia en Français

  • Double checked locking — Шаблон проектирования Блокировка с двойной проверкой Double checked locking Описан в Design Patterns Нет Double checked locking (блокировка с двойной проверкой) шаблон проектирования, применяющийся в параллельном программировании. Он… …   Википедия

  • Блокировка с двойной проверкой — Double checked locking (блокировка с двойной проверкой) шаблон проектирования применяющийся в параллельном программировании. Он предназначен для уменьшения накладных расходов, связанных с получением блокировки. Сначала проверяется условие… …   Википедия

  • Anti-pattern — (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster, Architekturmuster...),… …   Deutsch Wikipedia

  • Antimuster — Anti Pattern (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster,… …   Deutsch Wikipedia

  • Antipattern — Anti Pattern (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster,… …   Deutsch Wikipedia

  • Negativmuster — Anti Pattern (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster,… …   Deutsch Wikipedia

  • Anti-Pattern — (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster, Architekturmuster...),… …   Deutsch Wikipedia

  • Singleton pattern — In software engineering, the singleton pattern is a design pattern used to implement the mathematical concept of a singleton, by restricting the instantiation of a class to one object. This is useful when exactly one object is needed to… …   Wikipedia

Share the article and excerpts

Direct link
Do a right-click on the link above
and select “Copy Link”