Vodič za ConcurrentMap

1. Pregled

Karte su prirodno jedan od najrasprostranjenijih stilova Java kolekcije.

I, što je važno, HashMap nije implementacija sigurna u nitima, dok Hashtable pruža sigurnost navoja sinkroniziranjem operacija.

Čak iako Hashtable je siguran na nit, nije vrlo učinkovit. Još jedan potpuno sinkroniziran Karta,Collections.synchronizedMap, ne pokazuje ni veliku učinkovitost. Ako želimo sigurnost niti s velikom propusnošću uz visoku istodobnost, ove implementacije nisu pravi put.

Da bi riješio problem, Okvir Java Collectionsuvedena Istodobna karta u Java 1.5.

Sljedeće rasprave temelje se na Java 1.8.

2. Istodobna karta

Istodobna karta je produžetak Karta sučelje. Cilj mu je pružiti strukturu i smjernice za rješavanje problema usklađivanja protoka s sigurnošću niti.

Nadjačavanjem nekoliko zadanih metoda sučelja, Istodobna karta daje smjernice za valjane implementacije za pružanje atomskih operacija s sigurnošću niti i memorijom dosljednih.

Nekoliko zadanih implementacija je poništeno, onemogućavajući null podrška ključ / vrijednost:

  • getOrDefault
  • za svakoga
  • zamjeni sve
  • computeIfAbsent
  • computeIfPresent
  • izračunati
  • sjediniti

Sljedeće Apis su također nadjačani kako bi podržali atomskost, bez zadane implementacije sučelja:

  • putIfAbsent
  • ukloniti
  • zamijeni (key, oldValue, newValue)
  • zamijeniti (ključ, vrijednost)

Ostatak radnji izravno se nasljeđuje u osnovi s Karta.

3. ConcurrentHashMap

ConcurrentHashMap je spreman za prodaju Istodobna karta provedba.

Za bolju izvedbu sastoji se od niza čvorova kao segmenata tablice (koji su prije bili segmenti tablice Java 8) ispod haube i uglavnom koristi CAS operacije tijekom ažuriranja.

Segmenti tablice se lijeno inicijaliziraju nakon prvog umetanja. Svaka se sekvenca može samostalno zaključati zaključavanjem prvog čvora u seriji. Operacije čitanja ne blokiraju, a sadržaj ažuriranja je minimiziran.

Potreban broj segmenata je u odnosu na broj niti koje pristupaju tablici, tako da ažuriranje u tijeku po segmentu većinu vremena neće biti duže od jednog.

Prije Java 8, potreban je broj „segmenata“ u odnosu na broj niti koje pristupaju tablici, tako da ažuriranje u tijeku po segmentu većinu vremena neće biti više od jednog.

Zato konstruktori, u usporedbi s HashMap, pruža ekstra istodobnostLevel argument za kontrolu broja procijenjenih niti koje će se koristiti:

javna ConcurrentHashMap (
javna ConcurrentHashMap (int InitiCapacity, float loadFactor, int concurrencyLevel)

Druga dva argumenta: početni kapacitet i loadFactor radio potpuno isto kao i HashMap.

Međutim, budući da Java 8, konstruktori su prisutni samo za povratnu kompatibilnost: parametri mogu utjecati samo na početnu veličinu karte.

3.1. Sigurnost navoja

Istodobna karta jamči dosljednost memorije na operacijama ključ / vrijednost u okruženju s više niti.

Radnje u niti prije stavljanja predmeta u Istodobna karta kao ključ ili vrijednost dogoditi se-prije radnje nakon pristupa ili uklanjanja tog objekta u drugoj niti.

Da bismo potvrdili, pogledajmo slučaj koji nije u skladu s memorijom:

@Test javna praznina givenHashMap_whenSumParallel_thenError () baca izuzetak {Map map = new HashMap (); Popis sumList = parallelSum100 (karta, 100); assertNotEquals (1, sumList .stream () .distinct () .count ()); long wrongResultCount = sumList .stream () .filter (num -> num! = 100) .count (); assertTrue (wrongResultCount> 0); } privatni popis paralelniSum100 (karta mape, int izvedba) baca InterruptedException {Lista sumList = novi ArrayList (1000); for (int i = 0; i <timesTimes; i ++) {map.put ("test", 0); ExecutorService executorService = Izvršitelji.newFixedThreadPool (4); za (int j = 0; j {za (int k = 0; k vrijednost + 1);}); } executorService.shutdown (); executorService.awaitTermination (5, TimeUnit.SECONDS); sumList.add (map.get ("test")); } return sumList; }

Za svakoga map.computeIfPresent paralelno djelovanje, HashMap ne pruža dosljedan prikaz onoga što bi trebala biti sadašnja cijela vrijednost, što dovodi do nedosljednih i neželjenih rezultata.

Što se tiče ConcurrentHashMap, možemo dobiti dosljedan i točan rezultat:

@Test javna praznina givenConcurrentMap_whenSumParallel_thenCorrect () baca izuzetak {Map map = new ConcurrentHashMap (); Popis sumList = parallelSum100 (karta, 1000); assertEquals (1, sumList .stream () .distinct () .count ()); long wrongResultCount = sumList .stream () .filter (num -> num! = 100) .count (); assertEquals (0, wrongResultCount); }

3.2. Nula Ključ / vrijednost

Najviše APIs koje pruža Istodobna karta ne dozvoljava null ključ ili vrijednost, na primjer:

@Test (očekuje se = NullPointerException.class) javna praznina givenConcurrentHashMap_whenPutWithNullKey_thenThrowsNPE () {concurrentMap.put (null, new Object ()); } @Test (očekuje se = NullPointerException.class) javna praznina givenConcurrentHashMap_whenPutNullValue_thenThrowsNPE () {concurrentMap.put ("test", null); }

Međutim, za izračunati * i sjediniti radnje, izračunata vrijednost može biti null, što znači da se mapiranje ključ / vrijednost uklanja ako je prisutno ili ostaje odsutno ako je prethodno bilo odsutno.

@Test javna praznina givenKeyPresent_whenComputeRemappingNull_thenMappingRemoved () {Objekt oldValue = novi objekt (); concurrentMap.put ("test", oldValue); concurrentMap.compute ("test", (s, o) -> null); assertNull (concurrentMap.get ("test")); }

3.3. Podrška za strujanje

Java 8 pruža Stream podrška u ConcurrentHashMap također.

Za razliku od većine metoda streama, skupne (sekvencijalne i paralelne) operacije omogućuju istodobnu izmjenu na siguran način. ConcurrentModificationException neće biti bačeno, što se također odnosi na njegove iteratore. Relevantno za potoke, nekoliko za svakoga*, traži, i smanjiti* Također su dodane metode za potporu bogatijim obilaženjima i operacijama smanjenja karata.

3.4. Izvođenje

Ispod haube, ConcurrentHashMap je donekle sličan HashMap, s pristupom podacima i ažuriranjem na temelju hash tablice (iako složenije).

I naravno, ConcurrentHashMap bi trebao donijeti mnogo bolje performanse u većini istodobnih slučajeva za preuzimanje i ažuriranje podataka.

Napišimo brzi mikro-mjerilo za dobiti i staviti izvedbu i usporedite to s Hashtable i Zbirke.sinkroniziranaKarta, izvodeći obje operacije 500 000 puta u 4 niti.

@Test public void givenMaps_whenGetPut500KTimes_thenConcurrentMapFaster () baca iznimku {Map hashtable = new Hashtable (); Karta synchronizedHashMap = Collections.synchronizedMap (nova HashMap ()); Karta concurrentHashMap = novo ConcurrentHashMap (); long hashtableAvgRuntime = timeElapseForGetPut (hashtable); dugo syncHashMapAvgRuntime = timeElapseForGetPut (synchronizedHashMap); dugo concurrentHashMapAvgRuntime = timeElapseForGetPut (concurrentHashMap); assertTrue (hashtableAvgRuntime> concurrentHashMapAvgRuntime); assertTrue (syncHashMapAvgRuntime> concurrentHashMapAvgRuntime); } private long timeElapseForGetPut (karta karte) baca InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool (4); long startTime = System.nanoTime (); for (int i = 0; i {for (int j = 0; j <500_000; j ++) {int value = ThreadLocalRandom .current () .nextInt (10000); String key = String.valueOf (value); map.put (ključ, vrijednost); map.get (ključ);}}); } executorService.shutdown (); executorService.awaitTermination (1, TimeUnit.MINUTES); return (System.nanoTime () - startTime) / 500_000; }

Imajte na umu da mikro-mjerila gledaju samo na jedan scenarij i nisu uvijek dobar odraz stvarnih rezultata.

To je rečeno, na OS X sustavu s prosječnim razvojnim sustavom vidimo prosječni rezultat uzorka za 100 uzastopnih pokretanja (u nanosekundama):

Hashtable: 1142,45 SynchronizedHashMap: 1273,89 ConcurrentHashMap: 230,2

U okruženju s više niti, gdje se očekuje da više niti pristupi zajedničkom Karta, ConcurrentHashMap je jasno poželjno.

Međutim, kad Karta je dostupan samo jednoj niti, HashMap može biti bolji izbor zbog svoje jednostavnosti i solidnih performansi.

3.5. Zamke

Operacije dohvaćanja uglavnom se ne blokiraju ConcurrentHashMap i moglo bi se preklapati s operacijama ažuriranja. Dakle, za bolju izvedbu, oni odražavaju samo rezultate posljednje dovršenih operacija ažuriranja, kako je navedeno u službenom Javadocu.

Treba imati na umu nekoliko drugih činjenica:

  • rezultati metoda agregatnog statusa uključujući veličina, prazno je, i sadržiVrijednost su obično korisni samo kada karta ne prolazi istodobna ažuriranja u drugim nitima:
@Test javna praznina givenConcurrentMap_whenUpdatingAndGetSize_thenError () baca InterruptedException {Runnable collectMapSizes = () -> {for (int i = 0; i {for (int i = 0; i <MAX_SIZE; i ++) {concurrentMap.put (String. if. I. I. ), i);}}; executorService.execute (updateMapData); executorService.execute (collectMapSizes); executorService.shutdown (); executorService.awaitTermination (1, TimeUnit.MINUTES); assertNotEEQUAL (MAX_SIZ.EQUAL (MAX_SIZ.EQUAL (MAX_SIZ.EQUAL (MAX_SIZ.GE. (MAX_SIZ.EZ. (MAX_SIZ.EZ. (MAX_SIZ.Ex. (mak. MAXI ) .intValue ()); assertEquals (MAX_SIZE, concurrentMap.size ());}

Ako su istodobna ažuriranja pod strogom kontrolom, agregatni status i dalje će biti pouzdan.

Iako ovi metode agregatnog statusa ne jamče točnost u stvarnom vremenu, možda su prikladne za potrebe praćenja ili procjene.

Imajte na umu da upotreba veličina() od ConcurrentHashMap treba zamijeniti sa mappingCount (), za potonju metodu vraća a dugo broje, iako se duboko u sebi temelje na istoj procjeni.

  • hashCode pitanjima: imajte na umu da upotreba mnogih tipki s potpuno istim hashCode () siguran je način za usporavanje izvedbe bilo koje hash tablice.

Za poboljšanje utjecaja kad su tipke Usporedive, ConcurrentHashMap može upotrijebiti poredbeni poredak među ključevima za pomoć u prekidu veza. Ipak, trebali bismo izbjegavati koristiti isti hashCode () koliko možemo.

  • iteratori su dizajnirani samo za upotrebu u jednoj niti jer pružaju slabu dosljednost umjesto brzog neuspješnog prelaska i nikad neće baciti ConcurrentModificationException.
  • zadani kapacitet početne tablice je 16 i prilagođen je navedenom razinom istodobnosti:
javna ConcurrentHashMap (int InitiCapacity, float loadFactor, int concurrencyLevel) {// ... if (InitiCapacity <concurrencyLevel) {InitiCapacity = concurrencyLevel; } // ...}
  • oprez pri funkcijama ponovnog mapiranja: iako možemo izvršiti operacije preslikavanja s predviđenim izračunati i sjediniti* metode, trebali bi biti brzi, kratki i jednostavni te se usredotočiti na trenutno mapiranje kako bismo izbjegli neočekivano blokiranje.
  • unosi ključeve ConcurrentHashMap nisu poredani, tako da za slučajeve kada je potrebno naručivanje, ConcurrentSkipListMap je prikladan izbor.

4. ConcurrentNavigableMap

U slučajevima kada je potrebno naručivanje ključeva, možemo koristiti ConcurrentSkipListMap, istodobna verzija TreeMap.

Kao dodatak za Istodobna karta, ConcurrentNavigableMap podržava ukupno uređenje ključeva (prema zadanim postavkama u rastućem redoslijedu) i istodobno je plovan. Metode koje vraćaju prikaze karte su nadjačane radi kompatibilnosti podudarnosti:

  • podkarta
  • headMap
  • tailMap
  • podkarta
  • headMap
  • tailMap
  • silaznaKarta

keySet () iteratori i spliteratori pogleda poboljšani su slabom dosljednošću memorije:

  • navigableKeySet
  • keySet
  • silazniKljučni set

5. ConcurrentSkipListMap

Prije smo pokrivali NavigableMap sučelje i njegova implementacija TreeMap. ConcurrentSkipListMap može se vidjeti skalabilna istodobna verzija TreeMap.

U praksi nema istodobne implementacije crveno-crnog stabla u Javi. Istodobna varijanta SkipLists provodi se u ConcurrentSkipListMap, pružajući očekivani prosječni log (n) vremenski trošak za sadržiKljuč, dobiti, staviti i ukloniti operacije i njihove varijante.

Pored TreeMapZnačajke, umetanje, uklanjanje, ažuriranje i pristup ključu osiguravaju sigurnost niti. Evo usporedbe s TreeMap prilikom istovremene navigacije:

@Test javna praznina givenSkipListMap_whenNavConcurrently_thenCountCorrect () baca InterruptedException {NavigableMap skipListMap = new ConcurrentSkipListMap (); int count = countMapElementByPollingFirstEntry (skipListMap, 10000, 4); assertEquals (10000 * 4, računaj); } @Test javna praznina givenTreeMap_whenNavConcurrently_thenCountError () baca InterruptedException {NavigableMap treeMap = new TreeMap (); int count = countMapElementByPollingFirstEntry (treeMap, 10000, 4); assertNotEquals (10000 * 4, računaj); } private int countMapElementByPollingFirstEntry (NavigableMap navigableMap, int elementCount, int concurrencyLevel) baca InterruptedException {for (int i = 0; i <elementCount * concurrencyLevel; i ++) {navigableMap.put (i, i); } AtomicInteger counter = novi AtomicInteger (0); ExecutorService executorService = Izvršitelji.newFixedThreadPool (concurrencyLevel); for (int j = 0; j {for (int i = 0; i <elementCount; i ++) {if (navigableMap.pollFirstEntry ()! = null) {counter.incrementAndGet ();}}}); } executorService.shutdown (); executorService.awaitTermination (1, TimeUnit.MINUTES); vratiti brojač.get (); }

Potpuno objašnjenje zabrinutosti oko izvedbe iza kulisa izvan je dosega ovog članka. Pojedinosti možete pronaći u ConcurrentSkipListMap Javadoc, koji se nalazi pod java / util / istovremeno u src.zip datoteka.

6. Zaključak

U ovom smo članku uglavnom predstavili Istodobna karta sučelje i značajke ConcurrentHashMap i pokriveni na ConcurrentNavigableMap jer je potrebno naručivanje ključeva.

Potpuni izvorni kod za sve primjere korištene u ovom članku možete pronaći u projektu GitHub.