Útmutató a ConcurrentMap-hoz

1. Áttekintés

Térképek természetesen a Java gyűjtemény egyik legszélesebb stílusa.

És ami fontos, HashMap nem egy szálbiztos megvalósítás, míg Hashtable szálbiztonságot nyújt a műveletek szinkronizálásával.

Annak ellenére Hashtable szálbiztos, nem túl hatékony. Egy másik teljesen szinkronizált Térkép,Collections.synchronizedMap, sem mutat nagy hatékonyságot. Ha nagy biztonsággal, nagy egyidejűséggel rendelkező szálbiztonságot akarunk, akkor ezek a megvalósítások nem a megfelelő út.

A probléma megoldása érdekében a Java Collections Frameworkbemutatott ConcurrentMap ban ben Java 1.5.

A következő viták a következőkön alapulnak Java 1.8.

2. ConcurrentMap

ConcurrentMap kiterjesztése a Térkép felület. Célja, hogy struktúrát és útmutatást nyújtson az áteresztőképesség és a menetbiztonság összeegyeztetésének problémájához.

Több felületi alapértelmezett módszer felülírásával ConcurrentMap útmutatásokat ad az érvényes megvalósításokról a szálbiztonság és az emlékezet-konzisztens atomi műveletek biztosítása érdekében.

Számos alapértelmezett megvalósítást felülírnak, letiltva a nulla kulcs / érték támogatás:

  • getOrDefault
  • az egyes
  • csereAll
  • computeIfAbsent
  • computeIfPresent
  • kiszámít
  • összeolvad

A következő API-k szintén felülírják az atomitás támogatását, alapértelmezett interfész-megvalósítás nélkül:

  • putIfAbsent
  • eltávolítani
  • csere (kulcs, oldValue, newValue)
  • csere (kulcs, érték)

A többi művelet közvetlenül öröklődik, és alapvetően összhangban áll a következőkkel Térkép.

3. ConcurrentHashMap

ConcurrentHashMap készen van a dobozon kívül ConcurrentMap végrehajtás.

A jobb teljesítmény érdekében csomópontok tömbjéből áll, mint táblázatcsomagok (korábban táblaszegmensek voltak előtte Java 8) a motorháztető alatt, és főleg a CAS műveleteket használja a frissítés során.

Az asztali vödrök lustán inicializálódnak, az első behelyezés után. Minden vödör függetlenül zárható, ha a vödör legelső csomópontját lezárja. Az olvasási műveletek nem blokkolják, és a frissítési tartalmak minimalizálódnak.

A szükséges szegmensek száma a táblához hozzáférő szálak számához viszonyítva van, így a folyamatban lévő frissítés szegmensenként legfeljebb egy lehet.

Előtt Java 8, a szükséges „szegmensek” száma a táblához hozzáférő szálak számához viszonyítva volt, így a folyamatban lévő frissítés szegmensenként nem több, mint egy.

Ezért a kivitelezők ehhez képest HashMap, biztosítja az extrát concurrencyLevel argumentum a becsült felhasználandó szálak számának ellenőrzésére:

public ConcurrentHashMap (
public ConcurrentHashMap (int kezdeti kapacitás, úszó terhelés tényező, int egyidejűség szint)

A másik két érv: kezdeti Kapacitás és terhelési tényező ugyanúgy dolgozott, mint HashMap.

Mivel azonban Java 8, a konstruktorok csak a visszamenőleges kompatibilitás miatt vannak jelen: a paraméterek csak a térkép kezdeti méretét befolyásolhatják.

3.1. Menetbiztonság

ConcurrentMap garantálja a memória konzisztenciáját a kulcs / érték műveleteknél több szálú környezetben.

Műveletek egy szálban, mielőtt egy objektumot a ConcurrentMap mint kulcs vagy érték történés előtt az objektum egy másik szálban való elérését vagy eltávolítását követő műveletek.

Ennek megerősítéséhez nézzünk meg egy memória-inkonzisztens esetet:

@Test public void givenHashMap_whenSumParallel_thenError () dobja a Kivételt {Map map = new HashMap (); Lista sumList = parallelSum100 (térkép, 100); assertNotEquals (1, sumList .stream () .distinct () .count ()); long wrongResultCount = sumList .stream () .filter (szám -> szám! = 100) .szám (); assertTrue (falseResultCount> 0); } privát Lista parallelSum100 (Térképtérkép, int végrehajtási idő) dobja az InterruptedException {List sumList = new ArrayList (1000); for (int i = 0; i <végrehajtási idők; i ++) {map.put ("teszt", 0); ExecutorService végrehajtóService = Executors.newFixedThreadPool (4); for (int j = 0; j {for (int k = 0; k értéke + 1);}); } executorService.shutdown (); végrehajtóService.awaitTermination (5, TimeUnit.SECONDS); sumList.add (map.get ("teszt")); } return sumList; }

Az egyes map.computeIfPresent párhuzamos cselekvés, HashMap nem ad következetes képet arról, hogy mi legyen a jelenlegi egész érték, ami következetlen és nemkívánatos eredményekhez vezet.

Ami ConcurrentHashMap, következetes és korrekt eredményt kaphatunk:

@Test public void givenConcurrentMap_whenSumParallel_thenCorrect () dobja a Kivételt {Map map = new ConcurrentHashMap (); Lista sumList = parallelSum100 (térkép, 1000); assertEquals (1, sumList .stream () .distinct () .count ()); long wrongResultCount = sumList .stream () .filter (szám -> szám! = 100) .szám (); assertEquals (0, falseResultCount); }

3.2. Nulla Kulcs érték

A legtöbb APIs által biztosított ConcurrentMap nem teszi lehetővé nulla kulcs vagy érték, például:

@Test (várható = NullPointerException.class) public void givenConcurrentHashMap_whenPutWithNullKey_thenThrowsNPE () {concurrentMap.put (null, new Object ()); } @Test (várható = NullPointerException.class) public void givenConcurrentHashMap_whenPutNullValue_thenThrowsNPE () {concurrentMap.put ("test", null); }

Azonban, mert kiszámít* és összeolvad műveletek esetén a kiszámított érték lehet nulla, amely azt jelzi, hogy a kulcs-érték leképezés eltávolításra kerül, ha van, vagy hiányzik, ha korábban hiányzott.

@Test public void givenKeyPresent_whenComputeRemappingNull_thenMappingRemoved () {Object oldValue = new Object (); concurrentMap.put ("teszt", oldValue); concurrentMap.compute ("teszt", (s, o) -> null); assertNull (concurrentMap.get ("teszt")); }

3.3. Adatfolyam támogatás

Java 8 biztosítja Folyam támogatás a ConcurrentHashMap is.

A legtöbb stream módszerrel ellentétben a tömeges (szekvenciális és párhuzamos) műveletek biztonságosan lehetővé teszik az egyidejű módosítást. ConcurrentModificationException nem dobják el, ami az iterátorokra is vonatkozik. Patakokhoz kapcsolódóan több az egyes*, keresés, és csökkenteni * módszerek is hozzáadódnak a gazdagabb bejáráshoz és a térképcsökkentéshez.

3.4. Teljesítmény

A motorháztető alatt, ConcurrentHashMap némileg hasonló a HashMap, adateléréssel és hash-tábla alapján történő frissítéssel (bár összetettebb).

És természetesen a ConcurrentHashMap sokkal jobb teljesítményt kell nyújtania a legtöbb egyidejű esetben az adatok visszakeresése és frissítése érdekében.

Írjunk egy gyors mikro-benchmarkot a kap és tedd teljesítményét, és hasonlítsa össze azzal Hashtable és Collections.synchronizedMap, mindkét műveletet 500 000-szer futtatva 4 szálon.

@Test public void givenMaps_whenGetPut500KTimes_thenConcurrentMapFaster () dobja a Kivételt {Map hashtable = new Hashtable (); Map synchronizedHashMap = Collections.synchronizedMap (új HashMap ()); Map concurrentHashMap = new ConcurrentHashMap (); long hashtableAvgRuntime = timeElapseForGetPut (hashtable); hosszú syncHashMapAvgRuntime = timeElapseForGetPut (synchronizedHashMap); long concurrentHashMapAvgRuntime = timeElapseForGetPut (concurrentHashMap); assertTrue (hashtableAvgRuntime> concurrentHashMapAvgRuntime); assertTrue (syncHashMapAvgRuntime> concurrentHashMapAvgRuntime); } privát long timeElapseForGetPut (térképes térkép) dobja az InterruptedException {ExecutorService végrehajtóService = Executors.newFixedThreadPool (4); hosszú startTime = System.nanoTime (); for (int i = 0; i {for (int j = 0; j <500_000; j ++) {int érték = ThreadLocalRandom .current () .nextInt (10000); String kulcs = String.valueOf (érték); map.put (kulcs, érték); map.get (kulcs);}}); } executorService.shutdown (); végrehajtóService.awaitTermination (1, TimeUnit.MINUTES); return (System.nanoTime () - startTime) / 500_000; }

Ne feledje, hogy a mikro-benchmarkok csak egyetlen forgatókönyvet vizsgálnak, és nem mindig tükrözik jól a valós teljesítményt.

Ez azt jelenti, hogy egy átlagos dev rendszerű OS X rendszeren 100 egymást követő futás átlagmintáját látjuk (nanoszekundumban):

Hashtable: 1142.45 SynchronizedHashMap: 1273.89 ConcurrentHashMap: 230.2

Többszálas környezetben, ahol több szál várhatóan hozzáfér egy közöshez Térkép, a ConcurrentHashMap egyértelműen előnyösebb.

Amikor azonban a Térkép csak egyetlen szálhoz érhető el, HashMap jobb választás lehet egyszerűsége és szilárd teljesítménye miatt.

3.5. Buktatók

A visszakeresési műveletek általában nem blokkolják ConcurrentHashMap és átfedhetik a frissítési műveletekkel. Tehát a jobb teljesítmény érdekében csak a legfrissebb frissítési műveletek eredményeit tükrözik, amint azt a hivatalos Javadoc is kimondta.

Számos egyéb tényt kell szem előtt tartani:

  • összesített állapot-módszerek eredményei, beleértve méret, üres, és tartalmazzaValue általában csak akkor hasznosak, ha a térképen nem zajlik egyidejű frissítés más szálakban:
A @Test public void givenConcurrentMap_whenUpdatingAndGetSize_thenError () dobja az InterruptedException {Runnable collectMapSizes = () -> {for (int i = 0; i {for (int i = 0; i <MAX_SIZE; i ++) {concurrentMap.put (String. ), i);}}; végrehajtóService.execute (updateMapData); végrehajtóService.execute (collectMapSizes); végrehajtóService.shutdown (); végrehajtóService.awaitTermination (1, TimeUnit.MINUTES); assertNotEquals (MAX_SIZE - 1 ) .intValue ()); assertEquals (MAX_SIZE, concurrentMap.size ());}

Ha az egyidejű frissítéseket szigorú ellenőrzés alatt tartják, az összesített állapot továbbra is megbízható.

Bár ezek az összesített státusz-módszerek nem garantálják a valós idejű pontosságot, monitoring vagy becslés céljából megfelelőek lehetnek.

Vegye figyelembe, hogy a méret() nak,-nek ConcurrentHashMap helyébe a következő lép: mappingCount (), az utóbbi módszerhez a hosszú bár mélyen ugyanazon a becslésen alapulnak.

  • hash kód számít: vegye figyelembe, hogy sok, pontosan ugyanolyan billentyűt használ hash kód() biztos módja bármely hash tábla teljesítményének lelassítására.

A hatás enyhítésére, amikor a kulcsok vannak Hasonló, ConcurrentHashMap összehasonlítási sorrendet használhat a kulcsok között a kapcsolatok megszakításához. Ennek ellenére kerülnünk kell ugyanezt hash kód() amennyit csak tudunk.

  • az iterátorokat csak egyetlen szálon történő használatra tervezték, mivel gyenge konzisztenciát nyújtanak, nem pedig gyorsan elbukó átmenetet, és soha nem fognak dobni ConcurrentModificationException.
  • az alapértelmezett kezdeti táblázatkapacitás 16, és a megadott egyidejűségi szint módosítja:
public ConcurrentHashMap (int kezdetiCapacity, float loadFactor, int concurrencyLevel) {// ... if (initialCapacity <concurrencyLevel) {initialCapacity = concurrencyLevel; } // ...}
  • óvatosság a funkciók újratervezésével kapcsolatban: bár a feltöltéssel elvégezhetünk újratervezési műveleteket kiszámít és összeolvad* módszereket, ezeket gyorsnak, rövidnek és egyszerűnek kell tartanunk, és a váratlan blokkolás elkerülése érdekében az aktuális feltérképezésre kell összpontosítanunk.
  • kulcsok ConcurrentHashMap nincsenek rendezve, ezért azokban az esetekben, amikor megrendelésre van szükség, ConcurrentSkipListMap megfelelő választás.

4. ConcurrentNavigableMap

Olyan esetekben, amikor kulcsok megrendelésére van szükség, használhatjuk ConcurrentSkipListMap, egyidejű változata TreeMap.

Kiegészítésként a ConcurrentMap, ConcurrentNavigableMap támogatja a kulcsok teljes rendezését (alapértelmezés szerint növekvő sorrendben), és egyidejűleg navigálható. A párhuzamosság kompatibilitása felülbírálja a térkép nézeteit visszaadó módszereket:

  • subMap
  • headMap
  • tailMap
  • subMap
  • headMap
  • tailMap
  • csökkenő térkép

keySet () A nézetek iterátorait és elosztóit gyengébb memória-konzisztencia jellemzi:

  • navigableKeySet
  • keySet
  • descendingKeySet

5. ConcurrentSkipListMap

Korábban már kitértünk rá NavigableMap felület és annak megvalósítása TreeMap. ConcurrentSkipListMap a skálázható egyidejű változata látható TreeMap.

A gyakorlatban a vörös-fekete fa egyidejű megvalósítása nincs a Java-ban. A. Egyidejű változata SkipLists évben kerül végrehajtásra ConcurrentSkipListMap, megadva a várható átlagos log (n) időköltséget a tartalmazzaKey, kap, tedd és eltávolítani műveletek és változataik.

Továbbá TreeMapA szálbiztonság garantálja a funkciókat, a kulcs behelyezését, eltávolítását, frissítését és elérését. Itt van egy összehasonlítás a következővel: TreeMap ha egyszerre navigál:

@Test public void givenSkipListMap_whenNavConcurrently_thenCountCorrect () dob InterruptedException {NavigableMap skipListMap = new ConcurrentSkipListMap (); int count = countMapElementByPollingFirstEntry (skipListMap, 10000, 4); assertEquals (10000 * 4, szám); } @Test public void givenTreeMap_whenNavConcurrently_thenCountError () dobja az InterruptedException {NavigableMap treeMap = új TreeMap (); int count = countMapElementByPollingFirstEntry (treeMap, 10000, 4); assertNotEquals (10000 * 4, szám); } private int countMapElementByPollingFirstEntry (NavigableMap navigableMap, int elementCount, int concurrencyLevel) dobja az InterruptedException {for (int i = 0; i <elementCount * concurrencyLevel; i ++) {navigableMap.put (i, i); } AtomicInteger számláló = új AtomicInteger (0); ExecutorService végrehajtóService = Executors.newFixedThreadPool (concurrencyLevel); for (int j = 0; j {for (int i = 0; i <elementCount; i ++) {if (navigableMap.pollFirstEntry ()! = null) {counter.incrementAndGet ();}}}}; } executorService.shutdown (); végrehajtóService.awaitTermination (1, TimeUnit.MINUTES); return counter.get (); }

A színfalak mögötti előadások teljes körű magyarázata meghaladja a cikk kereteit. A részletek megtalálhatók itt: ConcurrentSkipListMap's Javadoc, amely alatt található java / util / concurrent ban,-ben src.zip fájl.

6. Következtetés

Ebben a cikkben elsősorban a ConcurrentMap interfész és a ConcurrentHashMap és be van takarva ConcurrentNavigableMap kulcsrendelés szükséges.

A cikkben használt összes példa teljes forráskódja megtalálható a GitHub projektben.


$config[zx-auto] not found$config[zx-overlay] not found