it-swarm-eu.dev

Je bezpečné získat hodnoty z Java.util.HashMap z více vláken (bez modifikace)?

Existuje případ, kdy bude vytvořena mapa, a jakmile bude inicializována, nebude nikdy znovu upravována. Bude však přístupný (pouze pomocí get (key)) z více vláken. Je bezpečné používat Java.util.HashMap tímto způsobem?

(V současné době jsem šťastně používal Java.util.concurrent.ConcurrentHashMap a nemám žádnou potřebu zlepšit výkon, ale jsem prostě zvědavý, jestli by stačilo jednoduché HashMap.) Proto tato otázka je ne "Který bych měl použít?" Ani Je to otázka výkonu, spíše otázka: "Bylo by to bezpečné?")

118
Dave L.

Váš idiom je bezpečný pokud a pouze pokud odkaz na HashMap je bezpečně publikováno. Spíše než cokoliv, co se týká vnitřních částí HashMap samotného, ​​se bezpečná publikace zabývá tím, jak konstrukční vlákno odkazuje na mapu viditelnou pro ostatní vlákna.

Jediným možným závodem je zde mezi konstrukcí HashMap a všemi čtenými vlákny, které k nim mohou přistupovat dříve, než je plně konstruován. Většina diskusí je o tom, co se stane se stavem mapového objektu, ale to je irelevantní, protože ho nikdy nemodifikujete - takže jediná zajímavá část je, jak je publikován odkaz HashMap

Představte si například, že mapu můžete publikovat takto:

class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}

... a v určitém okamžiku je setMap() nazývána mapou a další vlákna používají SomeClass.MAP pro přístup k mapě a zkontrolujte, zda je toto null podobné:

HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
  .. use the map
} else {
  .. some default behavior
}

To je není bezpečné, i když se zdá, že je. Problém je v tom, že neexistuje žádný stane se před vztah mezi množinou SomeObject.MAP a následným čtením na jiném vlákně, takže čtení vlákno může vidět částečně vytvořenou mapu. To může do značné míry dělat cokoliv a dokonce i v praxi to dělá věci jako vkládá čtenářské vlákno do nekonečné smyčky .

Chcete-li mapu bezpečně publikovat, musíte vytvořit vztah stane se před mezi zápis reference do HashMap (tj. Publikace) a následných čtenářů tohoto odkazu (tj. spotřeby). Pohodlně, existuje jen několik snadno zapamatovatelných způsobů, jak dosáhnout to[1]:

  1. Vyměňte odkaz prostřednictvím řádně uzamčeného pole ( JLS 17.4.5 )
  2. Pro inicializaci obchodů použijte statický inicializátor ( JLS 12.4 )
  3. Vyměňte odkaz prostřednictvím volatile pole ( JLS 17.4.5 ), nebo jako důsledek tohoto pravidla, přes třídy AtomicX
  4. Inicializujte hodnotu do konečného pole ( JLS 17,5 ).

Ty nejzajímavější pro váš scénář jsou (2), (3) a (4). Konkrétně (3) se vztahuje přímo na kód, který jsem použil výše: pokud transformujete prohlášení MAP na:

public static volatile HashMap<Object, Object> MAP;

pak je vše košer: čtenáři, kteří vidí hodnotu non-null, mají nutně vztah stane se před s úložištěm MAP a proto vidí všechny obchody spojené s inicializací mapy.

Ostatní metody mění sémantiku vaší metody, protože oba (2) (pomocí statického initalizer) a (4) (pomocí final) znamenají, že nemůžete nastavit MAP dynamicky za běhu. Pokud to neuděláte třeba, pak stačí deklarovat MAP jako static final HashMap<> a máte zaručenou bezpečnou publikaci.

V praxi jsou pravidla jednoduchá pro bezpečný přístup k "nikdy nemodifikovaným objektům":

Pokud publikujete objekt, který není inherentně neměnný (jako ve všech polích deklarovaných final) a:

  • Objekt, který bude přiřazen v okamžiku deklarace, již můžete vytvořita: stačí použít pole final (včetně static final pro statické členy).
  • Chcete-li objekt přiřadit později, po zobrazení odkazu: použijte volatilní poleb.

A je to!

V praxi je velmi efektivní. Například použití pole static final umožňuje JVM předpokládat, že hodnota je po celou dobu životnosti programu beze změny a výrazně ji optimalizuje. Použití final členských polí dovolí nejvíce architekturám číst pole způsobem, který je ekvivalentní čtení normálního pole a neinhibuje další optimalizacec

Konečně, použití volatile má nějaký vliv: na mnoha architekturách není potřeba žádná hardwarová bariéra (např. X86, konkrétně ty, které neumožňují číst čtení čtení), ale některé optimalizace a přeskupování se nemusí vyskytnout při kompilaci - ale tento účinek je obecně malý. Výměnou skutečně získáte více, než jste požadovali - nejenže můžete bezpečně publikovat jednu HashMap, ale můžete uložit tolik dalších nezměněných HashMaps, kolik chcete ke stejnému odkazu a ujistit se, že všichni čtenáři uvidí bezpečně publikované mapa.

Pro více podrobností, odkazovat se na Shipilev nebo toto FAQ Manson a Goetz .


[1] Přímé citování z shipilev .


a To zní komplikovaně, ale tím myslím, že můžete přiřadit odkaz na dobu výstavby - buď v místě deklarace, nebo v konstruktoru (členské pole) nebo statickém iniciátoru (statická pole).

b Volitelně můžete použít metodu synchronized pro získání/set nebo AtomicReference nebo něco, ale mluvíme o minimální práci, kterou můžete udělat.

c Některé architektury s velmi slabými paměťovými modely (já se dívám na vy, Alpha) mohou vyžadovat určitý druh přečtené bariéry před final čtení - ale tyto jsou dnes velmi vzácné.

31
BeeOnRope

Jeremy Manson, bůh, pokud jde o model paměti Java, má na toto téma třídílný blog - protože v podstatě se ptáte na otázku "Je bezpečný přístup k neměnnému HashMapu" - odpověď na to je ano. Musíte však odpovědět na predikát této otázce, která je - "Je můj HashMap neměnný". Odpověď by vás mohla překvapit - Java má relativně komplikovaný soubor pravidel pro určení neměnnosti.

Pro více informací o tomto tématu si přečtěte příspěvky blogu Jeremy:

Část 1 o nezměnitelnosti v Javě: http://jeremymanson.blogspot.com/2008/04/immutability-in-Java.html

Část 2 o nezměnitelnosti v Javě: http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-2.html

Část 3 o nezměnitelnosti v Javě: http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-3.html

70
Taylor Gautier

Čtení jsou bezpečná z hlediska synchronizace, ale nikoli z hlediska paměti. To je něco, co je široce nedorozuměno mezi vývojáři Java, včetně zde na Stackoverflow. (Dodržujte hodnocení tato odpověď pro důkaz.)

Pokud máte spuštěny jiné podprocesy, nemusí zobrazit aktualizovanou kopii nástroje HashMap, pokud z aktuálního podprocesu neexistuje žádná paměť. Zapisování paměti probíhá prostřednictvím použití synchronizovaných nebo volatilních klíčových slov nebo prostřednictvím použití některých konstruktů souběžnosti Java.

Viz článek Briana Goetze o novém modelu paměti Java podrobnosti. 

34
Heath Borders

Po trošce víc jsem to našel v Java doc (zvýraznění miny):

Tato implementace není synchronizována . Pokud více podprocesů Přistupuje k hašovací mapě souběžně a při Nejméně jeden z vláken strukturně modifikuje mapu , Musí být externě synchronizován . (Strukturální Modifikace je jakákoliv operace, která Přidává nebo odstraňuje jedno nebo více mapování; Pouze změnu hodnoty přidružené Klíčem, který instance již neobsahuje strukturu .

Zdá se, že to znamená, že to bude bezpečné, za předpokladu, že je to pravdivé.

10
Dave L.

Jedna poznámka je, že za určitých okolností může get () z nesynchronizovaného HashMap způsobit nekonečnou smyčku. To může nastat, pokud souběžný put () způsobí rehash mapy.

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

9
Alex Miller

Je tu však důležitý twist. Přístup k mapě je bezpečný, ale obecně není zaručeno, že všechny podprocesy uvidí přesně stejný stav (a tedy hodnoty) nástroje HashMap. To se může stát v systémech s více procesory, kde úpravy HashMapu provedené jedním vláknem (např. Ten, který jej naplnil) mohou sedět v mezipaměti CPU a nebudou vidět vlákny běžícími na jiných CPU, dokud není operace paměťového ploteru prováděla zajišťování koherence cache. Specifikace jazyka Java je v tomto případě jednoznačná: řešením je získat zámek (synchronizovaný (...)), který vyzařuje operaci paměťového plotu. Takže pokud jste si jisti, že po naplnění HashMapu každý z vláken získá JAKÝKOLIV zámek, pak je od tohoto okamžiku v pořádku přístup k HashMap z libovolného vlákna, dokud se HashMap znovu nezmění.

8
Alexander

Podle http://www.ibm.com/developerworks/Java/library/j-jtp03304/ # Bezpečnost inicializace můžete vytvořit HashMap konečné pole a po dokončení konstruktoru bude bezpečně publikován.

... Pod novým paměťovým modelem existuje něco podobného vztahům mezi zápisem konečného pole v konstruktoru a počátečním zatížením sdíleného odkazu na tento objekt v jiném vlákně. .____.] ...

4
bodrin

Takže scénář, který jste popsali, je to, že musíte do Mapy vložit spoustu dat. Jeden přístup, který je "bezpečný" (což znamená, že vynucujete, že se s ním opravdu zachází jako s neměnným), je nahradit odkaz Collections.unmodifiableMap(originalMap), když jste připraveni učinit jej neměnným.

Příklad toho, jak špatné mapy mohou selhat, pokud se používají souběžně, a navrhovaný postup, který jsem zmínil, naleznete v této položce chyby: bug_id = 6423457

3
Will

Buďte upozorněni, že i v jednovláknovém kódu nemusí být výměna ConcurrentHashMap za HashMap bezpečná. ConcurrentHashMap zakáže null jako klíč nebo hodnotu. HashMap jim nezakazuje (neptejte se).

Takže v nepravděpodobné situaci, kdy by váš stávající kód mohl přidat null do kolekce během instalace (pravděpodobně v případě selhání nějakého druhu), nahrazení kolekce podle popisu změní funkční chování.

To znamená, že pokud neděláte nic jiného, ​​souběžné čtení z HashMapu je bezpečné. 

[Edit: by "concurrent reads", mám na mysli, že neexistují také souběžné úpravy.

Další odpovědi vysvětlují, jak to zajistit. Jedním ze způsobů je vytvořit mapu neměnnou, ale není to nutné. Model paměti JSR133 například explicitně definuje spouštění podprocesu, který má být synchronizovanou akcí, což znamená, že změny provedené v podprocesu A před jeho spuštěním podprocesu B jsou viditelné v podprocesu B.

Mým záměrem není odporovat těmto podrobnějším odpovědím o modelu paměti Java. Tato odpověď má za cíl poukázat na to, že mezi otázkami souběžnosti existuje alespoň jeden rozdíl API mezi ConcurrentHashMap a HashMap, který by mohl odepsat dokonce i jednovláknový program, který nahradil jeden s druhým.]

1
Steve Jessop

Pokud je inicializace a každý put synchronizován, uložíte.

Následující kód je uložen, protože classloader se postará o synchronizaci:

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

Následující kód je uložen, protože zápis volatile se postará o synchronizaci.

class Foo {
  volatile HashMap<String, String> map;
  public void init() {
    final HashMap<String, String> tmp = new HashMap<>();
    tmp.put("A","A");
    // writing to volatile has to be after the modification of the map
    this.map = tmp;
  }
}

To bude také fungovat, pokud je proměnná člena konečná, protože finální je také volatilní. A pokud je metoda konstruktorem.

0
TomWolk

http://www.docjar.com/html/api/Java/util/HashMap.Java.html

zde je zdroj pro HashMap. Jak můžete říci, není tam absolutně žádný blokovací/mutex kód.

To znamená, že i když je dobré číst z HashMapu v situaci s více podprocesy, určitě bych použil ConcurrentHashMap, pokud by existovalo více zápisů.

Zajímavé je, že jak .NET HashTable, tak slovník <K, V> mají zabudovaný synchronizační kód.

0
FlySwat