zde je moje vlastní třída pro jednobarevný vzor. v tomto kódu používám dvojitě kontrolované zamykání, jak je uvedeno níže. Jak jsem četl mnoho příspěvků na nějakém zdroji, říkají, že dvojitá kontrola je užitečná, protože zabraňuje tomu, aby dvě souběžná vlákna spuštěná ve stejnou dobu vytvořila dva různé objekty.
public class DoubleCheckLocking < public static class SearchBox < private static volatile SearchBox searchBox; // private constructor private SearchBox() <>// static method to get instance public static SearchBox getInstance() < if (searchBox == null) < // first time lock synchronized (SearchBox.class) < if (searchBox == null) < // second time lock searchBox = new SearchBox(); >> > return searchBox; > > Stále výše uvedenému kódu moc nerozumím. Jaký je problém, pokud dvě vlákna společně spouštějí stejný řádek kódu, když je instance null?
if (searchBox == null) < synchronized (SearchBox.class) < if (searchBox == null) < searchBox = new SearchBox(); >> > Když se to objeví. obě dvě vlákna uvidí, že objekt je null. pak se obě synchronizují. a pak, znovu zkontrolují a stále vidí, že je to nulové. a vytvořit dva různé objekty. OOOPS. Vysvětlete mi to prosím. Co jsem špatně pochopil? Děkuji
18.1k 34 34 zlaté odznaky 125 125 stříbrné odznaky 206 206 bronzové odznaky
zeptal se 7. srpna 2013 v 2:40
29.9k 53 53 zlaté odznaky 176 176 stříbrné odznaky 250 250 bronzové odznaky
Přesně stejná otázka zde pro svět C#.
Února 14, v 2018 22: 30
tady je můj názor na to taky
Dubna 12, v 2020 4: 09
4 odpovědi 4
Ne, protože získáváte zámek na SearchBox.class , do synchronizovaného bloku vstoupí vždy pouze jedno vlákno. První vlákno tedy vstoupí, zjistí, že searchBox je nulové a vytvoří jej a poté opustí synchronizovaný blok, poté do bloku vstoupí druhé vlákno a zjistí, že vyhledávací pole není null, protože první vlákno jej již vytvořilo, takže nevytvoří nový instance vyhledávacího pole .
Dvojité zaškrtnutí se používá k tomu, aby se zabránilo získání zámku při každém provedení kódu. Pokud volání neprobíhá společně, první podmínka selže a spuštění kódu neprovede uzamčení, čímž se šetří zdroje.
15.3k 11 11 zlaté odznaky 81 81 stříbrné odznaky 109 109 bronzové odznaky
odpověděl 7. 2013. 2 v 46:XNUMX
Arun P Johny Arun P Johny
386k 66 66 zlaté odznaky 529 529 stříbrné odznaky 531 531 bronzové odznaky
Moc o tom nevím, ale zřejmě je tento typ dvojitě zkontrolovaného zámku rozbitý. Podle toho to nefunguje tak, jak byste očekávali
7. srpna 2013 ve 2:49
@WilliamMorrison díky. to jsem ještě neviděl. teď to prochází
7. srpna 2013 ve 2:50
vlastně, tento příklad je ne zlomený. v paměťovém modelu jdk 1.5+, díky čemuž je referenční volatilní (jako v kódu OP) «opraví» dvojitě kontrolovaný uzamykací vzor.
7. srpna 2013 ve 2:53
Jaká by byla nevýhoda, kdyby byla metoda deklarována jako synchronizovaná pouze s jednou kontrolou null?
20. července 2016 ve 6:00
@nikhilvora, pokud je synchronizace na úrovni metody uzamčení, všechny ostatní statické metody ve třídě budou také zablokovány. je to výkonnostní hit.
Květen 30, 2022 na 23: 43
Podívejme se na tento kód:
1 if (searchBox == null) < 2 synchronized (SearchBox.class) < 3 if (searchBox == null) < 4 searchBox = new SearchBox(); 5 >6 > 7 > Zkusme se nad tím zamyslet. Řekněme, že máme dvě vlákna A a B a předpokládejme, že alespoň jedno z nich dosáhne řádku 3 a zjistí, že searchBox == null je true . Dvě vlákna nemůže oba jsou na řádku 3 současně kvůli synchronizovanému bloku. To je klíč abyste pochopili, proč funguje dvojitá kontrola zamykání. Musí tedy platit, že buď A nebo B prošly synchronizovaně jako první. Bez ztráty obecnosti řekněme, že to vlákno je A . Poté, co uvidí searchBox == null je pravdivé, vstoupí do těla příkazu a nastaví searchBox na novou instanci SearchBox . Poté případně opustí synchronizovaný blok. Nyní bude řada na B, aby vstoupila: pamatujte, že B byla zablokována a čekala, až A odejde. Nyní, když vstoupí do bloku, bude sledovat searchBox . Ale A zůstane jen s nastavením searchBox na nenulovou hodnotu. Hotovo.
Mimochodem, v Javě je nejlepším způsobem implementace singletonu použití jednoprvkového typu enum. Z efektivní Javy:
I když tento přístup ještě nebyl široce přijat, jednoprvkový typ výčtu je nejlepší způsob, jak implementovat singleton.
Double-Checked Locking je široce citován a používán jako efektivní metoda pro implementaci líné inicializace ve vícevláknovém prostředí.
Bohužel nebude spolehlivě fungovat nezávisle na platformě, když je implementován v Javě, bez dodatečné synchronizace. Při implementaci v jiných jazycích, jako je C++, závisí na paměťovém modelu procesoru, přeuspořádání prováděném kompilátorem a interakci mezi kompilátorem a synchronizační knihovnou. Protože žádná z nich není specifikována v jazyce, jako je C++, lze jen málo říci o situacích, ve kterých bude fungovat. Explicitní paměťové bariéry mohou být použity, aby to fungovalo v C++, ale tyto bariéry nejsou dostupné v Javě.
Chcete-li nejprve vysvětlit požadované chování, zvažte následující kód:
// Třída verze s jedním vláknem Foo // další funkce a členové. >
Pokud by byl tento kód použit ve vícevláknovém kontextu, mnoho věcí by se mohlo pokazit. Nejspíše dva nebo více Pomocník objekty mohou být přiděleny. (Další problémy přineseme později). Řešením je jednoduše synchronizovat getHelper() metoda:
// Správná třída vícevláknové verze Foo // další funkce a členové. >
Výše uvedený kód provádí synchronizaci pokaždé getHelper() je nazýván. Dvojitě zkontrolovaný zamykací idiom se snaží vyhnout synchronizaci po přidělení pomocníka:
// Nefunkční vícevláknová verze // "Double-Checked Locking" idiom class Foo pomocník návratu; > // další funkce a členové. >
Bohužel tento kód prostě nefunguje v přítomnosti optimalizačních kompilátorů nebo multiprocesorů se sdílenou pamětí.
To nefunguje
Existuje mnoho důvodů, proč to nefunguje. Prvních pár důvodů, které popíšeme, je zřejmější. Poté, co to pochopíte, budete možná v pokušení vymyslet způsob, jak „opravit“ dvakrát kontrolovaný idiom zamykání. Vaše opravy nebudou fungovat: existují jemnější důvody, proč vaše oprava nebude fungovat. Pochopte tyto důvody, vymyslete lepší opravu a stále to nebude fungovat, protože existují ještě jemnější důvody.
Spousta velmi chytrých lidí nad tím strávila spoustu času. Tady je No Way aby to fungovalo, aniž by bylo nutné, aby každé vlákno, které přistupuje k pomocnému objektu, provádělo synchronizaci.
První důvod, proč to nefunguje
Nejzřejmějším důvodem, proč to nefunguje, je, že zápisy, které inicializují soubor Pomocník objekt a zápis do pomocník pole může být provedeno nebo vnímáno mimo provoz. Vlákno, které vyvolává getHelper() tedy může vidět nenulový odkaz na pomocný objekt, ale vidět výchozí hodnoty polí pomocného objektu, spíše než hodnoty nastavené v konstruktoru.
Pokud kompilátor vloží volání konstruktoru, pak zápisy, které inicializují objekt, a zápis do pomocník pole lze libovolně změnit pořadí, pokud kompilátor dokáže, že konstruktor nemůže vyvolat výjimku nebo provést synchronizaci.
I když kompilátor nezmění pořadí těchto zápisů, na víceprocesoru může procesor nebo paměťový systém změnit pořadí těchto zápisů, jak je vnímáno vláknem běžícím na jiném procesoru.
Testovací případ, který ukazuje, že to nefunguje
Pavel Jakubík našel příklad použití dvojitě zkontrolovaného zamykání, které nefungovalo správně. Mírně vyčištěná verze tohoto kódu je k dispozici zde.
Při spuštění v systému používajícím Symantec JIT to nefunguje. Zejména kompiluje Symantec JIT
singletons[i].reference = new Singleton();
na následující (všimněte si, že Symantec JIT používá systém přidělování objektů založený na popisovačích).
0206106A mov eax,0F97E78h 0206106F volejte 01F6B210 ; přidělit prostor pro ; Singleton, vrátit výsledek v eax 02061074 mov dword ptr [ebp],eax ; EBP je &singletons[i].reference; zde uložit nezkonstruovaný objekt. 02061077 mov ecx,dword ptr [eax] ; dereference rukojeti na ; získat nezpracovaný ukazatel 02061079 mov dword ptr [ecx],100h ; Další 4 řádky jsou 0206107F mov dword ptr [ecx+4],200h ; Singletonův inline konstruktor 02061086 mov dword ptr [ecx+8],400h 0206108D mov dword ptr [ecx+0Ch],0F84030h
Jak vidíte, zadání do singletons[i].odkaz se provádí před voláním konstruktoru pro Singleton. To je zcela legální podle stávajícího modelu paměti Java a také legální v C a C++ (protože ani jeden z nich nemá model paměti).
Oprava, která nefunguje
Vzhledem k výše uvedenému vysvětlení řada lidí navrhla následující kód:
// (Stále) Nefunkční vícevláknová verze // idiom class "Double-Checked Locking" Foo // uvolnění vnitřního synchronizačního zámku helper = h; >> pomocník pro návrat; > // další funkce a členové. >
Tento kód vkládá konstrukci objektu Helper do vnitřního synchronizovaného bloku. Intuitivní myšlenkou je, že v místě, kde je synchronizace uvolněna, by měla existovat paměťová bariéra, která by měla zabránit změně pořadí inicializace Pomocník objekt a přiřazení k terénnímu pomocníkovi.
Bohužel tato intuice je naprosto mylná. Pravidla pro synchronizaci tímto způsobem nefungují. Pravidlo pro monitorexit (tj. uvolnění synchronizace) je, že akce před monitorexit musí být provedeny před uvolněním monitoru. Neexistuje však žádné pravidlo, které by říkalo, že akce po monitorexitu nelze provést před uvolněním monitoru. Je naprosto rozumné a legální, aby kompilátor přesunul zadání pomocník = h; uvnitř synchronizovaného bloku, v takovém případě jsme zpět tam, kde jsme byli předtím. Mnoho procesorů nabízí instrukce, které provádějí tento druh jednosměrné paměťové bariéry. Změna sémantiky tak, aby vyžadovala uvolnění zámku, aby byla plná paměťová bariéra, by měla omezení výkonu.
Další opravy, které nefungují
Existuje něco, co můžete udělat, abyste přinutili zapisovač, aby provedl úplnou obousměrnou paměťovou bariéru. To je hrubé, neefektivní a je téměř zaručeno, že po revizi modelu paměti Java nebude fungovat. Toto nepoužívejte. V zájmu vědy jsem dal popis této techniky na samostatnou stránku. Nepoužívejte jej.
Nicméně, i když vlákno, které inicializuje pomocný objekt, provádí plnou paměťovou bariéru, stále to nefunguje.
Problém je v tom, že na některých systémech vlákno, které vidí nenulovou hodnotu pro pomocník pole také potřebuje provádět paměťové bariéry.
Proč? Protože procesory mají své vlastní kopie paměti uložené v místní mezipaměti. Na některých procesorech, pokud procesor neprovádí instrukci koherence mezipaměti (např. paměťová bariéra), lze čtení provádět ze zastaralých místně uložených kopií, i když jiné procesory používaly paměťové bariéry k vynucení svých zápisů do globální paměti.
Vytvořil jsem samostatnou webovou stránku s diskusí o tom, jak se to může stát na procesoru Alpha.
Stojí to za ty potíže?
U většiny aplikací jsou náklady na jednoduchou výrobu getHelper() metoda synchronizovaná není vysoká. O tomto druhu podrobných optimalizací byste měli uvažovat pouze v případě, že víte, že to pro aplikaci způsobuje značnou režii.
Velmi často bude mít mnohem větší dopad více chytrosti na vyšší úrovni, jako je použití vestavěného mergesortu namísto zpracování výměnného řazení (viz benchmark SPECJVM DB).
Aby to fungovalo pro statické singletony
Pokud je singleton, který vytváříte, statický (tj. bude pouze jeden Pomocník vytvořený), na rozdíl od vlastnosti jiného objektu (např. bude existovat jeden Pomocník pro každého foo objektu, existuje jednoduché a elegantní řešení.
Stačí definovat singleton jako statické pole v samostatné třídě. Sémantika jazyka Java zaručuje, že pole nebude inicializováno, dokud na pole nebude odkazováno, a že každé vlákno, které k poli přistoupí, uvidí všechny zápisy vyplývající z inicializace tohoto pole.
Bude fungovat pro 32bitové primitivní hodnoty
Ačkoli idiom dvojité kontroly zamykání nelze použít pro odkazy na objekty, může fungovat pro 32bitové primitivní hodnoty (např. int’s nebo float’s). Všimněte si, že to nefunguje pro dlouhé nebo dvojité, protože nesynchronizované čtení/zápis 64bitových primitiv není zaručeno, že budou atomické.
// Správné dvojité zamykání pro 32bitová primitiva class Foo návrat h; > // další funkce a členové. >
Ve skutečnosti, za předpokladu, že funkce computeHashCode vždy vrátila stejný výsledek a neměla žádné vedlejší účinky (tj. idempotentní), můžete se dokonce zbavit veškeré synchronizace.
// Líná inicializace 32bitová primitiva // Bezpečné pro vlákna, pokud je computeHashCode idempotentní třída Foo návrat h; > // další funkce a členové. >
Aby to fungovalo s explicitními paměťovými bariérami
Pokud máte explicitní instrukce pro paměťovou bariéru, je možné provést dvojitou kontrolu zamykacího vzoru. Pokud například programujete v C++, můžete použít kód z knihy Douga Schmidta a kol.:
// Implementace C++ s explicitními paměťovými bariérami // Měla by fungovat na jakékoli platformě, včetně DEC Alphas // Z "Vzorů pro souběžné a distribuované objekty", // podle šablony Douga Schmidta TYP * Singleton::instance (void) return tmp; >
Oprava zamykání s dvojitou kontrolou pomocí místního úložiště vláken
Alexander Terekhov (TEREKHOV@de.ibm.com) přišel s chytrým návrhem na implementaci dvojitě kontrolovaného zamykání pomocí lokálního úložiště vláken. Každé vlákno uchovává místní příznak vlákna, který určuje, zda vlákno provedlo požadovanou synchronizaci.
class Foo private final void createHelper() // Jakákoli nenulová hodnota by zde fungovala jako argument perThreadInstance.set(perThreadInstance); >>
Výkon této techniky dost závisí na tom, jakou implementaci JDK máte. V implementaci Sunu 1.2 byly ThreadLocal velmi pomalé. Jsou výrazně rychlejší v 1.3 a očekává se, že budou ještě rychlejší v 1.4. Doug Lea analyzoval výkon některých technik pro implementaci líné inicializace.
Pod novým modelem paměti Java
Upevnění dvojitého zamykání pomocí Volatile
Díky této změně může být idiom Double-Checked Locking uveden do provozu deklarováním pomocník pole být nestálé. Tento nefunguje pod JDK4 a staršími.
// Pracuje se sémantikou získání/uvolnění pro volatilní // Rozbité pod aktuální sémantikou pro volatilní třídu Foo > pomocník pro návrat; >>
Dvojitá kontrola zamykání neměnných objektů
Pokud je Helper neměnný objekt, takže všechna pole Helper jsou konečná, pak bude dvojitě zkontrolované zamykání fungovat, aniž byste museli používat nestálá pole. Myšlenka je taková, že odkaz na neměnný objekt (jako je String nebo Integer) by se měl chovat v podstatě stejně jako int nebo float; čtení a zápis odkazů na neměnné objekty jsou atomické.
- Reality Check, Douglas C. Schmidt, C++ Report, SIGS, sv. 8, č. 3, březen 1996.
- Double-Checked Locking: Optimalizační vzor pro efektivní inicializaci a přístup k objektům bezpečným pro vlákna, Douglas Schmidt a Tim Harrison. 3. ročník konference Pattern Languages of Program Design, 1996
- Lazy instantiation, Philip Bishop a Nigel Warren, JavaWorld Magazine
- Programování Java vláken v reálném světě, část 7, Allen Holub, Javaworld Magazine, duben 1999.
- Java 2 Performance and Idiom Guide, Craig Larman a Rhett Guthrie, str. 100.
- Java v praxi: Designové styly a idiomy pro efektivní Javu, Nigel Warren a Philip Bishop, str. 142.
- Pravidlo 99, Prvky stylu Java, Allan Vermeulen, Scott Ambler, Greg Bumgardner, Eldon Metz, Trvor Misfeldt, Jim Shur, Patrick Thompson, Referenční knihovna SIGS
- Globální proměnné v Javě s Singleton Pattern, Wiebe de Jong, Gamelan














