Java ConcurrentMap - Java ConcurrentMap
A Java programozási nyelv Java Collections Framework 1.5-ös és újabb verziói meghatározzák és megvalósítják az eredeti szokásos egyszálas Térképeket, valamint az új szálbiztos Térképeket, amelyek megvalósítják az java.util.concurrent.ConcurrentMapinterfészt más egyidejű interfészek között. A Java 1.6-ban az java.util.NavigableMapinterfészt hozzáadták, kibővítették java.util.SortedMap, és az java.util.concurrent.ConcurrentNavigableMapinterfészt alfelület-kombinációként adták hozzá.
Java Map Interfaces
Az 1.8-as verziójú térképi interfész ábra az alábbi alakú. A halmazok a megfelelő Maps aleseteinek tekinthetők, amelyekben az értékek mindig egy adott konstansok, amelyeket figyelmen kívül lehet hagyni, bár a Set API megfelelő, de másképp nevezett módszereket használ. Alul található a java.util.concurrent.ConcurrentNavigableMap, amely többszörös öröklés.
Végrehajtások
ConcurrentHashMap
A java.util.Map felületen meghatározott rendezetlen hozzáférés esetén a java.util.concurrent.ConcurrentHashMap megvalósítja a java.util.concurrent.ConcurrentMap programot. A mechanizmus hash-hozzáférés egy hash-táblához a bejegyzések listájával, minden bejegyzés tartalmaz kulcsot, értéket, hash-ot és egy következő hivatkozást. A Java 8-at megelőzően több zár volt, mindegyik sorosítva hozzáférést adott a táblázat egy szegmenséhez. A Java 8-ban a natív szinkronizálást maguk a listák fejei használják, és a listák kis fákká mutálódhatnak, amikor a szerencsétlen hash ütközések miatt túl nagyra nőnek. Ezenkívül a Java 8 optimista módon használja az összehasonlítás és beállítás primitívet az első fejek táblázatba helyezéséhez, ami nagyon gyors. Az előadás O (n), de néha késések vannak, amikor újragyakorlás szükséges. Miután a hash tábla kibővült, soha nem zsugorodik, ami memória „szivárgáshoz” vezethet a bejegyzések eltávolítása után.
ConcurrentSkipListMap
A java.util.NavigableMap felület által definiált rendelt hozzáféréshez a java.util.concurrent.ConcurrentSkipListMap került hozzáadásra a Java 1.6-ban, és megvalósítja a java.util.concurrent.ConcurrentMap-ot és a java.util.concurrent.ConcurrentNavigableMap-ot is. Ez egy kihagyási lista, amely zár nélküli technikákat használ egy fa elkészítéséhez. A teljesítmény O (log (n)).
Ctrie
- Ctrie Trie alapú zár nélküli fa.
Párhuzamos módosítási probléma
A Java 1.5 java.util.concurrent csomag által megoldott egyik probléma az egyidejű módosítás. Az általa biztosított gyűjteményosztályokat több szál megbízhatóan használhatja.
Az összes Thread által megosztott, nem egyidejű térképnek és más gyűjteménynek valamilyen explicit zárolást kell használnia, például natív szinkronizálást az egyidejű módosítás megakadályozása érdekében, különben a program logikájából meg kell tudni bizonyítani, hogy egyidejű módosítás nem történhet meg. A Térkép több szálon történő egyidejű módosítása néha tönkreteszi a Térkép adatstruktúráinak belső konzisztenciáját, ami olyan hibákhoz vezet, amelyek ritkán vagy kiszámíthatatlanul jelentkeznek, és amelyeket nehéz felismerni és kijavítani. Ezenkívül egy szál egyidejű módosítása egy másik szál vagy szálak olvasási hozzáférésével néha kiszámíthatatlan eredményeket hoz az olvasó számára, bár a Térkép belső konzisztenciája nem romlik meg. Külső programlogika használata az egyidejű módosítás megakadályozására növeli a kód bonyolultságát és kiszámíthatatlan hibakockázatot hoz létre a meglévő és a jövőbeli kódban, bár lehetővé teszi a nem egyidejű Gyűjtemények használatát. Azonban sem a zárak, sem a programlogika nem képes összehangolni azokat a külső szálakat, amelyek érintkezésbe kerülhetnek a Gyűjteményrel.
Módosító számlálók
A párhuzamos módosítási probléma megoldása érdekében a nem egyidejű Map megvalósítások és más Gyűjtemények belső módosítási számlálókat használnak, amelyek az olvasás előtt és után konzultálnak a változások figyelésére: az írók növelik a módosítás számlálókat. Állítólag ezzel a mechanizmussal egyidejű módosítást kell észlelni, amely egy java.util.ConcurrentModificationException-t dob, de nem garantált, hogy minden esetben előfordul, és nem szabad rá hivatkozni. A számláló karbantartása szintén teljesítménycsökkentő. Teljesítmény okokból a számlálók nem ingatagak, ezért nem garantált, hogy a változtatások továbbterjednek a szálak között.
Collections.synchronizedMap ()
Az egyidejű módosítási probléma egyik megoldása egy adott burkoló osztály használata, amelyet egy gyár biztosít a Gyűjteményekben: public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)amely egy meglévő, nem szálbiztos Térképet burkol egy belső mutexen szinkronizáló módszerekkel. A más típusú gyűjteményekhez is vannak csomagolók. Ez egy részleges megoldás, mert még mindig lehetséges, hogy az alapul szolgáló Térképet véletlenül is elérhetik azok a Threads-ok, amelyek kibontott referenciákat tartanak vagy szereznek be. Ezenkívül az összes gyűjtemény megvalósítja a java.lang.Iterableszinkronizált csomagolású Térképeket és más csomagolt gyűjteményeket, amelyek nem biztosítanak szinkronizált iterátorokat, így a szinkronizálás az ügyfélkódra marad, ami lassú és hibára hajlamos, és nem várható, hogy más felhasználók másolják őket. a szinkronizált Térkép. Az iteráció teljes időtartamát is védeni kell. Ezenkívül egy kétszer különböző helyekre csomagolt térkép különböző belső mutex objektumokkal rendelkezik, amelyeken a szinkronizálás működik, lehetővé téve az átfedést. A delegáció teljesítménycsökkentő, de a modern Just-in-Time fordítók gyakran erősen beilleszkednek, korlátozva a teljesítmény csökkenését. Így működik a csomagolás a burkoló belsejében - a mutex csak egy végső objektum, m pedig a végső becsomagolt térkép:
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
Az iteráció szinkronizálása az alábbiak szerint ajánlott, azonban ez szinkronizál a burkolón, nem pedig a belső mutexen, lehetővé téve az átfedést:
Map<String, String> wrappedMap = Collections.synchronizedMap(map);
...
synchronized (wrappedMap) {
for (String s : wrappedMap.keySet()) {
// some possibly long operation executed possibly
// many times, delaying all other accesses
}
}
Natív szinkronizálás
Bármely Map biztonságosan használható egy többszálas rendszerben annak biztosításával, hogy minden hozzá való hozzáférését a Java szinkronizációs mechanizmus kezeli:
Map<String, String> map = new HashMap<String, String>();
...
// Thread A
// Use the map itself as the lock. Any agreed object can be used instead.
synchronized(map) {
map.put("key","value");
}
..
// Thread B
synchronized (map) {
String result = map.get("key");
...
}
...
// Thread C
synchronized (map) {
for (Entry<String, String> s : map.entrySet()) {
/*
* Some possibly slow operation, delaying all other supposedly fast operations.
* Synchronization on individual iterations is not possible.
*/
...
}
}
ReentrantReadWriteLock
A java.util.concurrent.ReentrantReadWriteLock segítségével használt kód hasonló a natív szinkronizáláshoz. A biztonság kedvéért azonban a zárakat egy try / last blokkban kell használni, hogy a korai kilépés, például a Exception dobás vagy a break / folytatás biztosan átmenjen a kinyitáson. Ez a technika jobb, mint a szinkronizálás, mert az olvasások átfedhetik egymást, új kérdés merül fel annak eldöntésében, hogy az írásoknak milyen prioritást élvezzenek az olvasásokkal szemben. Az egyszerűség kedvéért egy java.util.concurrent.ReentrantLock használható helyette, ami nem tesz különbséget olvasás / írás között. A zárakon több művelet lehetséges, mint a szinkronizálással, például tryLock() és tryLock(long timeout, TimeUnit unit).
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
final ReadLock readLock = lock.readLock();
final WriteLock writeLock = lock.writeLock();
..
// Thread A
try {
writeLock.lock();
map.put("key","value");
...
} finally {
writeLock.unlock();
}
...
// Thread B
try {
readLock.lock();
String s = map.get("key");
..
} finally {
readLock.unlock();
}
// Thread C
try {
readLock.lock();
for (Entry<String, String> s : map.entrySet()) {
/*
* Some possibly slow operation, delaying all other supposedly fast operations.
* Synchronization on individual iterations is not possible.
*/
...
}
} finally {
readLock.unlock();
}
Konvojok
A kölcsönös kizárásnak van egy konvojzár- problémája, amelyben a szálak felhalmozódhatnak egy zárra, ami miatt a JVM-nek drága várósorokat kell fenntartania és a várakozó szálakat „parkolnia” kell. Drága egy szál parkolása és leállítása, és lassú kontextusváltás léphet fel. A kontextuskapcsolók mikroszekundumról milliszekundumra igényelnek, míg a Map saját alapműveletei általában nanoszekundumokat vesznek igénybe. A teljesítmény a Threads teljesítményének kis töredékéig csökkenhet, ha a verseny növekszik. Ha a zárnak nincs vagy alig van versenye, akkor a teljesítmény hatása csekély, kivéve a zár versenytesztjét. A modern JVM-ek beillesztik a zárkód nagy részét, csak néhány utasításra redukálva, így a vitathatatlan esetet nagyon gyorsan megtartva. Az olyan újrabeilleszkedő technikák, mint a natív szinkronizálás vagy a java.util.concurrent.ReentrantReadWriteLock, azonban extra teljesítménycsökkentő poggyászt tartalmaznak a reentrancia mélységének fenntartása érdekében, ami kihat a vitátlan esetre is. A konvoj problémája enyhülni látszik a modern JVMS segítségével, de a lassú kontextusváltással el lehet rejteni: ebben az esetben a késleltetés növekedni fog, de az áteresztőképesség továbbra is elfogadható. Több száz szál esetén a 10 ms-os környezetváltási idő másodpercek alatt késleltetést eredményez.
Több mag
A kölcsönös kizárási megoldások nem használják ki a többmagos rendszer számítási teljesítményének minden előnyét, mert a Térképkódban egyszerre csak egy szál engedélyezett. A Java Collections Framework és mások által biztosított egyidejű Maps megvalósításai néha több magot használnak ki a Lock free programozási technikák alkalmazásával. A zárolás nélküli technikák olyan műveleteket használnak, mint az összehasonlítani a JavaA számos osztályban, például az AtomicReference-nél elérhető, összehasonlítandó AndSet () intrinsic módszer, hogy egyes Map-belső struktúrák feltételesen frissüljenek atomosan. Az összehasonlításAndSet () primitívet a JCF osztályokban olyan natív kód egészíti ki, amely képes összehasonlítani az algoritmusok egyes objektumainak speciális belső részein az algoritmusok összehasonlítását („nem biztonságos” hozzáférést használva). A technikák összetettek, gyakran támaszkodnak a szálak közötti kommunikáció szabályaira, amelyeket az illékony változók adnak, az események előtti relációra, a zárolás nélküli „újrapróbálkozási ciklusok” speciális fajtáira (amelyek nem olyanok, mint a spin-zárak, mivel mindig előrelépést eredményeznek) . Az CompareAndSet () speciális processzor-specifikus utasításokra támaszkodik. Bármely Java-kód más célokra is felhasználhatja az összehasonlító AndSet () metódust különféle egyidejű osztályokon, hogy elérje a zárolás nélküli vagy akár a várakozás nélküli párhuzamosságot, amely véges késleltetést biztosít. A zár nélküli technikák sok gyakori esetben egyszerűek, és néhány egyszerű kollekcióval, például halmokkal.
A diagram azt mutatja, hogy a Collections.synchronizedMap () használatával történő szinkronizálás a szokásos HashMap (lila) csomagolásával nem annyira méretezhető, mint a ConcurrentHashMap (piros). A többi a rendezett ConcurrentNavigableMaps AirConcurrentMap (kék) és ConcurrentSkipListMap (CSLM zöld). (A lapos foltok olyan újraváltások lehetnek, amelyek nagyobb méretű asztalokat készítenek, mint az Óvoda, és a ConcurrentHashMap több helyet foglal. Megjegyzés: az y tengelyen azt kell mondani, hogy „helyezi a K” értéket. A rendszer 8 magos i7 2,5 GHz, -Xms5000m-rel a GC megakadályozására). A GC és a JVM folyamatbővítése jelentősen megváltoztatja a görbéket, és egyes belső Lock-Free technikák szemetet okoznak.
Kiszámítható késés
A kölcsönös kizárási megközelítések másik problémája az, hogy valamilyen egyszálas kód által feltételezett teljes atomitás feltételezése sporadikus, elfogadhatatlanul hosszú szálközi késéseket okoz egyidejű környezetben. Különösen az iterátorok és az ömlesztett műveletek, például a putAll () és mások, a Térkép méretével arányos időt vehetnek igénybe, késleltetve más szálakat, amelyek kiszámíthatóan alacsony késéssel számolnak a nem ömlesztett műveleteknél. Például egy többszálas webszerver nem engedélyezheti egyes válaszok késleltetését más szálak hosszan futó iterációival, amelyek más kéréseket hajtanak végre, amelyek egy adott értéket keresnek. Ehhez kapcsolódik az a tény, hogy a Térképet lezáró szálaknak valójában nincs semmilyen követelményük a zárolásról való lemondásra, és a tulajdonosi szál végtelen hurkja továbbterjesztheti a végleges blokkolást más szálakra. Lassú tulajdonos A szálakat néha meg lehet szakítani. A hash-alapú térképeket szintén spontán késedelem éri az újravetés során.
Gyenge konzisztencia
A java.util.concurrency csomagok megoldása az egyidejű módosítási problémára, a konvojproblémára, a kiszámítható késleltetési problémára és a többmagos problémára egy gyenge konzisztenciának nevezett építészeti választás tartozik. Ez a választás azt jelenti, hogy az olyan olvasások, mint a get (), akkor sem blokkolódnak, ha a frissítések folyamatban vannak, és még akkor is megengedett, ha a frissítések átfedésben vannak egymással és az olvasásokkal. A gyenge konzisztencia lehetővé teszi például, hogy a ConcurrentMap tartalma megváltozzon annak egyetlen szál általi ismétlése során. Az iterátorokat úgy tervezték, hogy egyszerre egy szál használhassa őket. Így például egy két, egymástól függő bejegyzést tartalmazó térképet következetlenül láthat az olvasó szála egy másik szál módosítása során. Annak a frissítésnek, amelynek állítólag egy bejegyzés kulcsát ( k1, v ) atomrá kell változtatnia egy bejegyzésre ( k2, v ), el kell végeznie egy eltávolítást ( k1 ), majd egy putet ( k2, v ), míg egy iteráció hiányozhat vagy nézze meg két helyen. A lekérések egy adott kulcs értékét adják vissza, amely az adott kulcs legutóbbi befejezett frissítését tükrözi . Tehát létezik egy „történik-előtt” kapcsolat.
A ConcurrentMaps nem tudja lezárni az egész táblázatot. Nincs lehetőség a ConcurrentModificationException lehetőségre, mint a nem egyidejű Térképek véletlenszerű egyidejű módosításakor. A size () metódus hosszú időt vehet igénybe, szemben a megfelelő, egyidejűleg nem használt Térképekkel és más gyűjteményekkel, amelyek általában tartalmaznak egy méretmezőt a gyors eléréshez, mert előfordulhat, hogy valamilyen módon be kell szkennelniük a teljes Térképet. Ha párhuzamos módosítások történnek, az eredmények tükrözik a Térkép állapotát valamikor, de nem feltétlenül egyetlen következetes állapotot jelentenek, ezért a méret (), az IsEmpty () és a tartalmazzaValue () lehet a legjobban csak megfigyelésre használni.
ConcurrentMap 1.5 módszerek
Van néhány olyan művelet, amelyet a ConcurrentMap nyújt, és amelyek nincsenek a Map-ben - amelyet kiterjeszt - a módosítások atomosságának lehetővé tétele érdekében. A csere ( K, v1, v2 ) megvizsgálja a v1 létezését a K által azonosított bejegyzésben, és csak akkor, ha megtalálható, akkor a v1 atomilag v2-re cserélődik . Az új ( k, v ) helyettesítés csak akkor tesz put ( k, v ) -t, ha k már szerepel a Térképen. Ezenkívül a putIfAbsent ( k, v ) csak akkor tesz put ( k, v ) -t, ha k még nincs a térképen, és a (k, v) eltávolítása csak akkor távolítja el a v bejegyzést, ha v jelen van. Ez az atomitás fontos lehet néhány többszálas felhasználási esetnél, de nem kapcsolódik a gyenge konzisztencia korlátozásához.
A ConcurrentMaps esetében az alábbiak atomi.
m.putIfAbsent (k, v) atom, de ekvivalens:
if (k == null || v == null)
throw new NullPointerException();
if (!m.containsKey(k)) {
return m.put(k, v);
} else {
return m.get(k);
}
m helyettesíti (k, v) atomi, de ekvivalens:
if (k == null || v == null)
throw new NullPointerException();
if (m.containsKey(k)) {
return m.put(k, v);
} else {
return null;
}
m.pótlás (k, v1, v2) atom, de ekvivalens:
if (k == null || v1 == null || v2 == null)
throw new NullPointerException();
if (m.containsKey(k) && Objects.equals(m.get(k), v1)) {
m.put(k, v2);
return true;
} else
return false;
}
m.remove (k, v) atom, de ekvivalens:
// if Map does not support null keys or values (apparently independently)
if (k == null || v == null)
throw new NullPointerException();
if (m.containsKey(k) && Objects.equals(m.get(k), v)) {
m.remove(k);
return true;
} else
return false;
}
ConcurrentMap 1.8 módszerek
Mivel a Map és a ConcurrentMap interfészek, új módszerek nem adhatók hozzájuk a megvalósítások megszakítása nélkül. A Java 1.8 azonban hozzáadta az alapértelmezett interfész-implementációk képességét, és hozzáadta a Map interfész néhány új módszer alapértelmezett megvalósítását a getOrDefault (Objektum, V), forEach (BiConsumer), ReplaceAll (BiFunction), computeIfAbsent (K, Function), computeIfPresent ( K, BiFunction), kiszámítja (K, BiFunction) és egyesíti (K, V, BiFunction). A Map alapértelmezett megvalósításai nem garantálják az atomosságot, de a ConcurrentMap felülbíráló alapértelmezéseiben ezek Lock free technikákat alkalmaznak az atomitás eléréséhez, és a meglévő ConcurrentMap megvalósítások automatikusan atomok lesznek. A zármentes technikák lassabbak lehetnek, mint a betonosztályok felülírása, ezért a betonosztályok dönthetnek úgy, hogy atomikusan alkalmazzák őket, vagy sem, és dokumentálják a párhuzamossági tulajdonságokat.
Zármentes atomitás
Lehetőség van Lock-free technikák alkalmazására a ConcurrentMaps alkalmazásával, mivel ezek kellően nagy konszenzusszámú, nevezetesen végtelen módszereket tartalmaznak, vagyis tetszőleges számú szál összehangolható. Ez a példa megvalósítható a Java 8 egyesítéssel (), de az általános Lock-free mintát mutatja, amely általánosabb. Ez a példa nem a ConcurrentMap belső részeihez kapcsolódik, hanem ahhoz, hogy az ügyfél kód hogyan használja a ConcurrentMap-ot. Például, ha a térképben egy értéket atomosan meg akarunk szorozni egy állandó C értékkel:
static final long C = 10;
void atomicMultiply(ConcurrentMap<Long, Long> map, Long key) {
for (;;) {
Long oldValue = map.get(key);
// Assuming oldValue is not null. This is the 'payload' operation, and should not have side-effects due to possible re-calculation on conflict
Long newValue = oldValue * C;
if (map.replace(key, oldValue, newValue))
break;
}
}
A putIfAbsent ( k, v ) akkor is hasznos, ha a kulcs bejegyzése hiányzik. Ez a példa megvalósítható a Java 8 számítással (), de az általános Lock-free mintát mutatja, amely általánosabb. A csere ( k, v1, v2 ) nem fogad el null paramétereket, ezért néha szükség van ezek kombinációjára. Más szavakkal, ha v1 nulla, akkor a putIfAbsent ( k, v2 ), máskülönben a csere ( k, v1, v2 ) lesz meghívva.
void atomicMultiplyNullable(ConcurrentMap<Long, Long> map, Long key) {
for (;;) {
Long oldValue = map.get(key);
// This is the 'payload' operation, and should not have side-effects due to possible re-calculation on conflict
Long newValue = oldValue == null ? INITIAL_VALUE : oldValue * C;
if (replaceNullable(map, key, oldValue, newValue))
break;
}
}
...
static boolean replaceNullable(ConcurrentMap<Long, Long> map, Long key, Long v1, Long v2) {
return v1 == null ? map.putIfAbsent(key, v2) == null : map.replace(key, v1, v2);
}
Történelem
A Java gyűjtemények keretrendszerét elsősorban Joshua Bloch tervezte és fejlesztette , és a JDK 1.2- ben mutatták be . Az eredeti párhuzamossági osztályok a Doug Lea gyűjtemény csomagjából származnak .
Lásd még
Hivatkozások
- Goetz, Brian; Joshua Bloch; Joseph Bowbeer; Doug Lea; David Holmes; Tim Peierls (2006). Java párhuzamosság a gyakorlatban . Addison Wesley. ISBN 0-321-34960-1. OL 25208908M .
- Lea, Doug (1999). Egyidejű programozás Java-ban: Tervezési alapelvek és minták . Addison Wesley. ISBN 0-201-31009-0. OL 55044M .
Külső linkek
- Gyűjtemények leckéi
- Java 6 gyűjtemény bemutatója - Jakob Jenkov, Kadafi Kamphulusa
- Tigris megszelídítése: A gyűjtemény-keret
- „The Collections Framework” (Oracle Java SE 8 dokumentáció)
- Josh Bloch "A Java oktatóanyagok - gyűjtemények"
- Milyen Java kollekciót használjak? - Praktikus folyamatábra a gyűjtemények kiválasztásának egyszerűsítése érdekében
- 'Melyik Java gyűjteményt használja?' - Janeve George


