A duplán ellenőrzött zárolás egy konkurens programtervezési minta,Schmidt, D et al. Pattern-Oriented Software Architecture Vol 2, 2000 pp. 353-363 amit arra használnak, hogy csökkentse a lockolás költségét egy előzetes lekérdezéssel, hogy fennáll-e a lockolás oka, és csak akkor lockol, ha tényleg szükséges.
A minta nem minden környezetben (hardver, nyelv, fordító) hasznos. Fennáll annak a veszélye, hogy a konkurens programozásra nem kellően felkészített fordító kioptimalizálja a második lekérdezést. Egyes esetekben antimintának tekinthető.David Bacon et al. The "Double-Checked Locking is Broken" Declaration.
Tipikusan lusta inicializációhoz vagy egykéhez használják. A lusta inicializáció lényege, hogy az első használat idejére halasztja el a létrehozást.
Használata C++11-ben C++11-ben nincs szükség az egykéhez duplán ellenőrzött zárolásra: Ha a vezérlés egy olyan deklarációba lép bele, ahol éppen konkurensen inicializálnak egy változót, akkor a szálnak várnia kell, amíg a létrehozás befejeződik.6.7 [stmt.dcl] p4 Tehát az egyke létrehozható úgy, mint:
Singleton& instance() { static Singleton s; return s; }
Ha mégis használni kell a mintát, mivel például a Visual Studio 2013 nem tartalmazza a konkurens programozáshoz használható nyelvi szabvány "Magic statics" nevű elemét, akkor megszerző-elengedő kerítést kell használni:Double-Checked Locking is Fixed In C++11
std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); if (tmp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release); m_instance.store(tmp, std::memory_order_relaxed);
return tmp; }
Használata Javában Tekintsük a következő kódrészletet:
// Single-threaded version class Foo { private Helper helper; public Helper getHelper() { if (helper == null) { helper = new Helper();
return helper;
// other functions and members... }
Konkurens környezetben ez nem működik, zárolni kell az objektumot; különben lehetséges, hogy két szál szimultán hívja a getHelper() metódust, amivel egyszerre próbálják létrehozni az objektumot, vagy az egyik egy még nem kész objektumot vesz használatba. A zárolás azt jelenti, hogy az metódust szinkronizálttá kell tenni:
// Correct but possibly expensive multithreaded version class Foo { private Helper helper; public synchronized Helper getHelper() { if (helper == null) { helper = new Helper();
return helper;
// other functions and members... }
ami drága, ezért nem szeretik. Akár 100-szor annyi időbe is kerülhet egy szinkronizált metódus hívása, mint egy nem szinkronizálté. Előny, hogy csak az első szál hozza létre az objektumot, és ezalatt a többi szál közül néhányat megvárakoztat, amíg az objektum el nem készül. Ezután minden hívás referenciát ad vissza az objektumra. Ekkor azonban már szükségtelen a zárolás, ezért sok programozó megpróbálta ezt elkerülni: Zárolás nélkül lekérdezzük, hogy a változó inicializálva van-e. Ha már inicializálva van, akkor vissza lehet térni. Különben megszerezzük a lockot, és újra megkérdezzük, inicializálva van-e. Ha igen, akkor vissza lehet térni. Különben inicializáljuk a változót, és úgy térünk vissza.
// Broken multithreaded version // "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... } Ez az algoritmus hatékonynak látszó megoldást ad a problémára, azonban nem olyan jó, mint amilyennek kinéz, mivel bekövetkezhetnek ilyen sorrendben is az események: Az A szál elkezdi létrehozni a példányt, de közben elveszik tőle a vezérlést. Egyes nyelvekben a létrehozás alatt álló objektum kimásolódik egy megosztott változóba. Ez Javában is így van, ha a konstruktor inline; ekkor az allokálás után, de még a inicializálás előtt kikerül az objektum referenciája a megosztott változóba. A B szál azt hiszi, hogy ez egy kész objektum, és visszaadja. A zárolással nem is próbálkozik, hanem úgy használja az objektumot, ahogy akarja, ami miatt a program hibásan kezd működni vagy összeomlik.
Korai Java verziókban a duplán ellenőrzött zárolás nehezen észrevehető és nehezen reprodukálható hibákat okozott, és nehéz volt megkülönböztetni a jó implementációt a hibástól. Problémák voltak a hordozhatósággal is, mert a különböző ütemezési stratégiák miatt nem mindenütt működött. Ezt a problémát a J2SE 5.0 oldotta meg a volatile kulcsszó bevezetésével. Ez azt biztosítja, hogy többszálú környezetben sem kezelik rosszabbul a volatile változót, mint egyszálú környezetben. Az új minta:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.htmlhttp://www.oracle.com/technetwork/articles/javase/bloch-effective-08-qa-140880.html
// Works with acquire/release semantics for volatile in Java 1.5 and later // Broken under Java 1.4 and earlier semantics for volatile class Foo { private volatile Helper helper; 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... }
A result helyi változó feleslegesnek látszik, azonban sokat gyorsíthat a programon. Ugyanis, ha a helper inicializálva van, akkor a volatile mezőt csak egyszer kell kiolvasni. Ez akár 25%-os javulást is eredményezhet.Joshua Bloch "Effective Java, Second Edition", p. 283-284
Ha a helper objektum statikus, akkor alternatíva az egyke lusta inicializálása.Brian Goetz et al. Java Concurrency in Practice, 2006 pp348 Lásd: Listing 16.6 a fent idézett szövegből:
// Correct lazy initialization in Java class Foo { private static class HelperHolder { public static final Helper helper = new Helper();
public static Helper getHelper() { return HelperHolder.helper;
Ez arra hagyatkozik, hogy a beágyazott osztályok addig nem töltődnek be, amíg nincs rájuk hivatkozás.
A módosíthatatlan objektumokat nem lehet elrontani; ezért ha a helper utólagos módosítására nincs szükség, akkor volatile helyett final használható: Javamemorymodel-discussion mailing list
public class FinalWrapper<T> { public final T value; public FinalWrapper(T value) { this.value = value;
public class Foo { private FinalWrapper<Helper> helperWrapper;
public Helper getHelper() { FinalWrapper<Helper> tempWrapper = helperWrapper;
if (tempWrapper == null) { synchronized(this) { if (helperWrapper == null) { helperWrapper = new FinalWrapper<Helper>(new Helper());
tempWrapper = helperWrapper;
return tempWrapper.value;
A tempWrapper helyi változóra korrektség miatt van szükség; a helperWrapper használata az ellenőrzéshez és a visszatérési értékhez problémás lehet a Java Memory Model által megengedett átrendezések miatt. Performance of this implementation is not necessarily better than the volatile
Használata a Microsoft .NET keretrendszerben A .NET keretrendszerekben a duplán ellenőrzött zárolás könnyen megvalósítható. Egy gyakran használt minta:
public class MySingleton { private static object myLock = new object(); private static volatile MySingleton mySingleton = null; // 'volatile' is unnecessary in .NET 2.0 and later
private MySingleton() {
public static MySingleton GetInstance() { if (mySingleton == null) { // 1st check lock (myLock) { if (mySingleton == null) { // 2nd (double) check mySingleton = new MySingleton(); // A .NET 1.1 implicit volatile-nak jelöli a mySingleton objektumot, // ami biztosítja a szükséges menmóriakorlátokat a konstruktorhívások // és a mySingletonnak való írás között. A zárolás által létrehozott korlátozás nem elégséges, // mivel az objektum a zár elengedése előtt láthatóvá válhat. A .NET 2.0-tól kezdve // a zárolás elégséges, és a volatile-ra nincs szükség.
// A zár azért nem volt elégséges, mert nem minden szál próbálta megszerezni. // Az olvasásra megszerző zárra a mySingleton tesztelése és használata között van szükség. // Ezt a védelmet azért kapta a mySingleton, mert 'volatile' volt. // A .NET 2.0-tól kezdve nem kell a, 'volatile'. return mySingleton;
Ebben a példában a lock hint a mySingleton objektum, ami akkor nem null, ha már elkészült és használatra készen áll. A .NET 4.0-ban bevezették a Lazy<T> osztályt, ami alapértelmezetten használja a duplán ellenőrzött zárolást (ExecutionAndPublication mód), és tárolja a konstruktor által dobott kivételt, vagy a Lazy<T>-nek átadott függvény eredményét is:
public class MySingleton { private static readonly Lazy<MySingleton> _mySingleton = new Lazy<MySingleton>(() => new MySingleton());
private MySingleton() { }
public static MySingleton Instance
get
return _mySingleton.Value;
Jegyzetek
Fordítás
Kategória:Programtervezési minták Kategória:Java-kódpéldák