Dvojitá kontrola blokování | |
---|---|
Dvojitě kontrolované zamykání | |
Popsáno v Návrhové vzory | Ne |
Dvojité zamykání je paralelní návrhový vzor navržený tak , aby snížil režii spojenou se získáním zámku. Nejprve je zkontrolována podmínka blokování bez jakékoli synchronizace; vlákno se pokusí získat zámek pouze v případě, že výsledek kontroly naznačuje, že potřebuje získat zámek.
V některých jazycích a/nebo na některých počítačích není možné tento vzor bezpečně implementovat. Proto se mu někdy říká anti-vzor . Tyto vlastnosti vedly k přísnému vztahu „ se stane před “ v Java Memory Model a C++ Memory Model.
Běžně se používá ke snížení režie implementace pomalé inicializace ve vícevláknových programech, jako je součást návrhového vzoru Singleton . Při líné inicializaci proměnné je inicializace zpožděna, dokud není hodnota proměnné potřebná ve výpočtu.
Zvažte následující kód Java převzatý z [1] :
// Jednovláknová verze class Foo { private Helper helper = null ; public Helper getHelper () { if ( helper == null ) helper = new Helper (); návrat pomocník ; } // a další členové třídy... }Tento kód nebude správně fungovat ve vícevláknovém programu. Metoda getHelper()musí získat zámek v případě, že je volána současně ze dvou vláken. Pokud pole helperještě nebylo inicializováno a dvě vlákna volají metodu současně getHelper(), pak se obě vlákna pokusí vytvořit objekt, což povede k vytvoření dalšího objektu. Tento problém je vyřešen pomocí synchronizace, jak ukazuje následující příklad.
// Správná, ale "drahá" vícevláknová verze class Foo { private Helper helper = null ; public synchronized Helper getHelper () { if ( helper == null ) helper = new Helper (); návrat pomocník ; } // a další členové třídy... }Tento kód funguje, ale přináší další režii synchronizace. První volání getHelper()vytvoří objekt a je getHelper()třeba synchronizovat pouze několik vláken, která budou volána během inicializace objektu. Po inicializaci je synchronizace při volání getHelper()redundantní, protože bude číst pouze proměnnou. Protože synchronizace může snížit výkon o faktor 100 nebo více, režie zamykání při každém volání této metody se zdá zbytečná: jakmile je inicializace dokončena, zámek již není potřeba. Mnoho programátorů se pokusilo tento kód optimalizovat takto:
Na intuitivní úrovni se tento kód zdá správný. Existují však některé problémy (v Javě 1.4 a dřívějších a nestandardních implementacích JRE), kterým je třeba se vyhnout. Představte si, že události ve vícevláknovém programu probíhají takto:
Jedním z nebezpečí používání dvojitě zkontrolovaného zamykání v J2SE 1.4 (a dřívějších) je to, že program často vypadá, že funguje správně. Za prvé, uvažovaná situace nebude nastávat příliš často; za druhé, je obtížné odlišit správnou implementaci tohoto vzoru od toho, který má popsaný problém. V závislosti na kompilátoru , alokaci procesorového času plánovačem vláknům a povaze ostatních běžících souběžných procesů se chyby způsobené nesprávnou implementací dvojitě kontrolovaného zamykání obvykle vyskytují náhodně. Reprodukce takových chyb je obvykle obtížná.
Problém můžete vyřešit pomocí J2SE 5.0 . Nová sémantika klíčových slov volatileumožňuje v tomto případě správně zvládnout zápis do proměnné. Tento nový vzor je popsán v [1] :
// Funguje s novou nestálou sémantikou // Nefunguje v Javě 1.4 a dřívějších kvůli nestálé sémantice class Foo { private volatile Helper = null ; public Helper getHelper () { if ( helper == null ) { synchronized ( this ) { if ( helper == null ) helper = new Helper (); } } return helper ; } // a další členové třídy... }Bylo navrženo mnoho dvojitě zkontrolovaných možností zamykání, které explicitně (prostřednictvím volatilních nebo synchronizačních) neindikují, že objekt je plně zkonstruován, a všechny jsou nesprávné pro Symantec JIT a starší Oracle JRE [2] [3] .
Microsoft potvrzuje [4] , že při použití klíčového slova volatile je bezpečné použít zamykací vzor Double check.
Následující kód Pythonu ukazuje příklad implementace líné inicializace v kombinaci s dvojitým zamykacím vzorem:
# vyžadovat Python2 nebo Python3 #-*- kódování: UTF-8 *-* importování závitů class SimpleLazyProxy : '''Inicializace líného objektu bezpečný pro vlákna''' def __init__ ( self , továrna ): self . __lock = navlékání . RLock () self . __obj = Žádné vlastní . __továrna = továrna def __call__ ( self ): '''funkce pro přístup ke skutečnému objektu pokud objekt není vytvořen, bude vytvořen''' # pokuste se získat "rychlý" přístup k objektu: obj = self . __obj pokud obj není None : # úspěšné! return obj else : # objekt možná ještě nebyl vytvořen se sebou samým . __lock : # získat přístup k objektu ve výhradním režimu: obj = self . __obj pokud obj není None : # se ukáže, že objekt již byl vytvořen. #nevytvářejte to znovu return obj else : # objekt ve skutečnosti ještě nebyl vytvořen. # pojďme to vytvořit! obj = já . __factory () sebe . __obj = obj vrátit obj __getattr__ = lambda self , jméno : \ getattr ( self (), jméno ) def lazy ( proxy_cls = SimpleLazyProxy ): '''dekorátor, který změní třídu na třídu s línou inicializací pomocí třídy Proxy''' class ClassDecorator : def __init__ ( self , cls ): # inicializace dekorátoru, # ale ne zdobené třídy a ne třídy proxy sebe . cls = cls def __call__ ( self , * args , ** kwargs ): # volání pro inicializaci třídy proxy # předat potřebné parametry třídě proxy # k inicializaci zdobené třídy return proxy_cls ( lambda : self . cls ( * args , ** kwargs )) vrátit ClassDecorator #jednoduchá kontrola: def test_0 (): print ( ' \t\t\t *** Spuštění testu ***' ) čas importu @lazy () # instance této třídy budou líně inicializované třídy TestType : def __init__ ( self , jméno ): print ( ' %s : Vytvořeno...' % jméno ) # uměle prodloužit dobu vytváření objektu # pro zvýšení konkurence vláken čas . spát ( 3 ) sebe . jméno = jméno print ( ' %s : Vytvořeno!' % jméno ) def test ( self ): tisk ( ' %s : Testování' % self . jméno ) # jedna taková instance bude interagovat s více vlákny test_obj = TestType ( 'Testovací objekt mezi vlákny' ) target_event = podprocesy . Event () def threads_target (): # funkce, kterou vlákna vykonají: # čekat na speciální událost target_event . počkat () # jakmile nastane tato událost - # všech 10 vláken současně přistoupí k testovacímu objektu # a v tuto chvíli je inicializován v jednom z vláken test_obj . test () # vytvořte těchto 10 vláken pomocí výše uvedeného algoritmu threads_target() threads = [] pro vlákno v rozsahu ( 10 ): thread = threading . vlákno ( target = thread_target ) vlákno . spustit () vlákna . připojit ( vlákno ) tisknout ( 'Dosud nebyly žádné přístupy k objektu' ) # počkej chvíli... čas . spát ( 3 ) # ...a spusťte test_obj.test() současně na všech vláknech print ( 'Spusťte událost pro použití testovacího objektu!' ) target_event . nastavit () # konec pro vlákno ve vláknech : vlákno . připojit se () tisknout ( ' \t\t\t *** Konec testu ***' )Designové vzory | |
---|---|
Hlavní | |
Generativní | |
Strukturální | |
Behaviorální | |
Paralelní programování |
|
architektonický |
|
Java EE šablony | |
Jiné šablony | |
knihy | |
Osobnosti |