Movatterモバイル変換


[0]ホーム

URL:


Jump to content
WikipediaThe Free Encyclopedia
Search

Double-checked locking

From Wikipedia, the free encyclopedia
Software design pattern

Insoftware engineering,double-checked locking (also known as "double-checked locking optimization"[1]) is asoftware design pattern used to reduce the overhead of acquiring alock by testing the locking criterion (the "lock hint") before acquiring the lock. Locking occurs only if the locking criterion check indicates that locking is required.

The original form of the pattern, appearing inPattern Languages of Program Design 3,[2] hasdata races, depending on thememory model in use, and it is hard to get right. Some consider it to be ananti-pattern.[3] There are valid forms of the pattern, including the use of thevolatile keyword in Java and explicit memory barriers in C++.[4]

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

Motivation and original pattern

[edit]

Consider, for example, this code segment in theJava programming language:[4]

// Single-threaded versionclassFoo{privatestaticHelperhelper;publicHelpergetHelper(){if(helper==null){helper=newHelper();}returnhelper;}// other functions and members...}

The problem is that this does not work when using multiple threads. Alock must be obtained in case two threads callgetHelper() 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.

Synchronizing with a lock can fix this, as is shown in the following example:

// Correct but possibly expensive multithreaded versionclassFoo{privateHelperhelper;publicsynchronizedHelpergetHelper(){if(helper==null){helper=newHelper();}returnhelper;}// other functions and members...}

This is correct and will most likely have sufficient performance. However, the first call togetHelper() 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 could in some extreme cases decrease performance by a factor of 100 or higher,[5] 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, including the authors of the double-checked locking design pattern, 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// original "Double-Checked Locking" idiomclassFoo{privateHelperhelper;publicHelpergetHelper(){if(helper==null){synchronized(this){if(helper==null){helper=newHelper();}}}returnhelper;}// 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 adata race. For example, consider the following sequence of events:

  1. ThreadA 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 apartially constructed object beforeA 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.[6]
  3. ThreadB notices that the shared variable has been initialized (or so it appears), and returns its value. Because threadB believes the value is already initialized, it does not acquire the lock. IfB uses the object before all of the initialization done byA is seen byB (either becauseA has not finished initializing it or because some of the initialized values in the object have not yet percolated to the memoryB uses (cache coherence)), the program will likely crash.

Most runtimes havememory 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 thecompiler, the interleaving of threads by thescheduler and the nature of otherconcurrent 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++

[edit]

For the singleton pattern, double-checked locking is not needed:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

— § 6.7 [stmt.dcl] p4

Singleton&getInstance(){staticSingletons;returns;}

C++11 and beyond also provide a built-in double-checked locking pattern in the form ofstd::once_flag andstd::call_once:

importstd;usingstd::once_flag;usingstd::optional;classSingleton{private:Singleton()=default;staticoptional<Singleton>instance;staticonce_flagflag;public:staticSingleton*getInstance(){std::call_once(Singleton::flag,[]()->void{instance.emplace(Singleton());});return&instance;}};

If one truly wishes to use the double-checked idiom instead of the trivially working example above (for instance becauseVisual Studio before the 2015 release did not implement the C++11 standard's language about concurrent initialization quoted above[7] ), one needs to use acquire and release fences:[8]

importstd;usingstd::atomic;usingstd::lock_guard;usingstd::mutex;classSingleton{private:Singleton()=default;staticatomic<Singleton*>instance;staticmutexm;public:staticSingleton*getInstance(){Singleton*p=instance.load(std::memory_order_acquire);if(!p){// 1st checklock_guard<mutex>lock(m);p=instance.load(std::memory_order_relaxed);if(!p){// 2nd (double) checkp=newSingleton();instance.store(p,std::memory_order_release);}}returnp;}~Singleton(){// cleanup logic}};

Usage in POSIX

[edit]

pthread_once() must be usedto 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

[edit]
packagemainimport"sync"vararrOncesync.Oncevararr[]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.funcgetArr()[]int{arrOnce.Do(func(){arr=[]int{0,1,2}})returnarr}funcmain(){// thanks to double-checked locking, two goroutines attempting to getArr()// will not cause double-initializationgogetArr()gogetArr()}

Usage in Java

[edit]

As ofJ2SE 5.0, thevolatile 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[3] and[4].

// Works with acquire/release semantics for volatile in Java 1.5 and later// Broken under Java 1.4 and earlier semantics for volatileclassFoo{privatevolatileHelperhelper;publicHelpergetHelper(){HelperlocalRef=helper;if(localRef==null){synchronized(this){localRef=helper;if(localRef==null){helper=localRef=newHelper();}}}returnlocalRef;}// other functions and members...}

Note thelocal variable "localRef", which seems unnecessary. The effect of this is that in cases wherehelper is already initialized (i.e., most of the time), the volatile field is only accessed once (due to "return localRef;" instead of "return helper;"), which can improve the method's overall performance by as much as 40 percent.[9]

Java 9 introduced theVarHandle 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 ofsequential consistency (field accesses no longer participate in the synchronization order, the global order of accesses to volatile fields).[10]

// Works with acquire/release semantics for VarHandles introduced in Java 9classFoo{privatevolatileHelperhelper;publicHelpergetHelper(){HelperlocalRef=getHelperAcquire();if(localRef==null){synchronized(this){localRef=getHelperAcquire();if(localRef==null){localRef=newHelper();setHelperRelease(localRef);}}}returnlocalRef;}privatestaticfinalVarHandleHELPER;privateHelpergetHelperAcquire(){return(Helper)HELPER.getAcquire(this);}privatevoidsetHelperRelease(Helpervalue){HELPER.setRelease(this,value);}static{try{MethodHandles.Lookuplookup=MethodHandles.lookup();HELPER=lookup.findVarHandle(Foo.class,"helper",Helper.class);}catch(ReflectiveOperationExceptione){thrownewExceptionInInitializerError(e);}}// other functions and members...}

If the helper object is static (one per class loader), an alternative is theinitialization-on-demand holder idiom[11] (See Listing 16.6[12] from the previously cited text.)

// Correct lazy initialization in JavaclassFoo{privatestaticclassHelperHolder{publicstaticfinalHelperhelper=newHelper();}publicstaticHelpergetHelper(){returnHelperHolder.helper;}}

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

Semantics offinal field in Java 5 can be employed to safely publish the helper object without usingvolatile:[13]

publicclassFinalWrapper<T>{publicfinalTvalue;publicFinalWrapper(Tvalue){this.value=value;}}publicclassFoo{privateFinalWrapper<Helper>helperWrapper;publicHelpergetHelper(){FinalWrapper<Helper>tempWrapper=helperWrapper;if(tempWrapper==null){synchronized(this){if(helperWrapper==null){helperWrapper=newFinalWrapper<Helper>(newHelper());}tempWrapper=helperWrapper;}}returntempWrapper.value;}}

The local variabletempWrapper is required for correctness: simply usinghelperWrapper for both null checks and the return statement could fail due to read reordering allowed under the Java Memory Model.[14] Performance of this implementation is not necessarily better than thevolatile implementation.

Usage in C#

[edit]

In .NET Framework 4.0, theLazy<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 toLazy<T>:[15]

publicclassMySingleton{privatestaticreadonlyLazy<MySingleton>_mySingleton=newLazy<MySingleton>(()=>newMySingleton());privateMySingleton(){}publicstaticMySingletonInstance=>_mySingleton.Value;}

See also

[edit]

References

[edit]
  1. ^Schmidt, D et al. Pattern-Oriented Software Architecture Vol 2, 2000 pp353-363
  2. ^Pattern languages of program design. 3(PDF) (Nachdr. ed.). Reading, Mass: Addison-Wesley. 1998.ISBN 978-0201310115.
  3. ^Gregoire, Marc (24 February 2021).Professional C++. John Wiley & Sons.ISBN 978-1-119-69545-5.
  4. ^abDavid Bacon et al.The "Double-Checked Locking is Broken" Declaration.
  5. ^Boehm, Hans-J (Jun 2005)."Threads cannot be implemented as a library"(PDF).ACM SIGPLAN Notices.40 (6):261–268.doi:10.1145/1064978.1065042. Archived fromthe original(PDF) on 2017-05-30. Retrieved2014-08-12.
  6. ^Haggar, Peter (1 May 2002)."Double-checked locking and the Singleton pattern". IBM. Archived fromthe original on 2017-10-27. Retrieved2022-05-19.
  7. ^"Support for C++11-14-17 Features (Modern C++)".
  8. ^Double-Checked Locking is Fixed In C++11
  9. ^Bloch, Joshua (2018).Effective Java (Third ed.). Addison-Wesley. p. 335.ISBN 978-0-13-468599-1.On my machine, the method above is about 1.4 times as fast as the obvious version without a local variable.
  10. ^"Chapter 17. Threads and Locks".docs.oracle.com. Retrieved2018-07-28.
  11. ^Brian Goetz et al. Java Concurrency in Practice, 2006 pp348
  12. ^Goetz, Brian; et al."Java Concurrency in Practice – listings on website". Retrieved21 October 2014.
  13. ^[1] Javamemorymodel-discussion mailing list
    Page not found – consider updating the link
  14. ^[2]Manson, Jeremy (2008-12-14)."Date-Race-Ful Lazy Initialization for Performance – Java Concurrency (&c)". Retrieved3 December 2016.
  15. ^Albahari, Joseph (2010)."Threading in C#: Using Threads".C# 4.0 in a Nutshell. O'Reilly Media.ISBN 978-0-596-80095-6.Lazy<T> actually implements […] double-checked locking. Double-checked locking performs an additional volatile read to avoid the cost of obtaining a lock if the object is already initialized.

External links

[edit]
Gang of Four
patterns
Creational
Structural
Behavioral
Concurrency
patterns
Architectural
patterns
Other
patterns
Books
People
Communities
See also
Retrieved from "https://en.wikipedia.org/w/index.php?title=Double-checked_locking&oldid=1309377649"
Categories:
Hidden categories:

[8]ページ先頭

©2009-2026 Movatter.jp