Java ConcurrentMap - Java ConcurrentMap

Java Collections Framework versiunea 1.5 a limbajului de programare Java și versiunile ulterioare definește și implementează Hărțile originale cu un singur fir obișnuit, precum și Hărți noi, sigure, care implementează java.util.concurrent.ConcurrentMapinterfața printre alte interfețe simultane. În Java 1.6, java.util.NavigableMapinterfața a fost adăugată, extinzându-se java.util.SortedMap, iar java.util.concurrent.ConcurrentNavigableMapinterfața a fost adăugată ca o combinație de subinterfață.

Interfețe Java Map

Diagrama interfeței Harta versiunea 1.8 are forma de mai jos. Seturile pot fi considerate sub-cazuri ale hărților corespunzătoare în care valorile sunt întotdeauna o constantă specială care poate fi ignorată, deși Set API folosește metode corespunzătoare, dar denumite diferit. În partea de jos este java.util.concurrent.ConcurrentNavigableMap, care este o moștenire multiplă.

Implementări

ConcurrentHashMap

Pentru acces neordonat, definit în interfața java.util.Map, java.util.concurrent.ConcurrentHashMap implementează java.util.concurrent.ConcurrentMap. Mecanismul este un acces hash la un tabel hash cu liste de intrări, fiecare intrare deținând o cheie, o valoare, hash și o următoare referință. Înainte de Java 8, existau mai multe blocări, fiecare acces serializând un „segment” al tabelului. În Java 8, sincronizarea nativă este utilizată pe capetele listelor, iar listele pot muta în copaci mici atunci când amenință să crească prea mult din cauza coliziunilor nefericite de hash. De asemenea, Java 8 folosește primitivul de comparare și setare optimist pentru a plasa capetele inițiale în tabel, ceea ce este foarte rapid. Performanța este O (n), dar există întârzieri ocazional atunci când este necesară re-spălarea. După ce tabelul hash se extinde, acesta nu se micșorează niciodată, ceea ce poate duce la o „scurgere” a memoriei după ce intrările sunt eliminate.

ConcurrentSkipListMap

Pentru acces comandat așa cum este definit de interfața java.util.NavigableMap, java.util.concurrent.ConcurrentSkipListMap a fost adăugat în Java 1.6 și implementează java.util.concurrent.ConcurrentMap și, de asemenea, java.util.concurrent.ConcurrentNavigableMap. Este o listă Skip care folosește tehnici fără blocare pentru a crea un copac. Performanța este O (log (n)).

Ctrie

  • Ctrie Un arbore fără blocare bazat pe trie.

Problemă de modificare concurentă

O problemă rezolvată de pachetul Java 1.5 java.util.concurrent este aceea a modificării concurente. Clasele de colectare pe care le oferă pot fi utilizate în mod fiabil de mai multe fire.

Toate hărțile non-concurente partajate cu fire și alte colecții trebuie să utilizeze o formă de blocare explicită, cum ar fi sincronizarea nativă, pentru a preveni modificarea concurentă, sau altfel trebuie să existe o modalitate de a demonstra din logica programului că modificarea concurentă nu poate avea loc. Modificarea concomitentă a unei hărți prin mai multe fire va distruge uneori consistența internă a structurilor de date din hartă, ducând la erori care se manifestă rar sau imprevizibil și care sunt dificil de detectat și de remediat. De asemenea, modificarea simultană de un fir cu acces de citire de un alt fir sau fire va da uneori rezultate imprevizibile cititorului, deși consistența internă a hărții nu va fi distrusă. Utilizarea logicii programului extern pentru a preveni modificarea simultană crește complexitatea codului și creează un risc imprevizibil de erori în codul existent și viitor, deși permite utilizarea colecțiilor non-concurente. Cu toate acestea, fie blocările, fie logica programului nu pot coordona firele externe care pot intra în contact cu colecția.

Contoare de modificare

Pentru a ajuta la problema de modificare concurentă, implementările de hărți non-concurente și alte colecții utilizează contoare de modificare interne care sunt consultate înainte și după o citire pentru a urmări modificările: scriitorii incrementează contoare de modificare. Se presupune că o modificare simultană este detectată de acest mecanism, aruncând o excepție java.util.ConcurrentModificationModification, dar nu este garantată să apară în toate cazurile și nu ar trebui să se bazeze pe ea. Întreținerea contorului este, de asemenea, un reductor de performanță. Din motive de performanță, contoare nu sunt volatile, deci nu este garantat că modificările aduse acestora vor fi propagate între fire.

Collections.synchronizedMap ()

O soluție la problema modificării concurente este utilizarea unei anumite clase de împachetare furnizată de o fabrică în colecții: public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)care înfășoară o hartă existentă, care nu este sigură cu fire, cu metode care se sincronizează pe un mutex intern. Există, de asemenea, ambalaje pentru celelalte tipuri de colecții. Aceasta este o soluție parțială, deoarece este încă posibil ca harta subiacentă să poată fi accesată din neatenție de către fire care păstrează sau obțin referințe neambalate. De asemenea, toate colecțiile implementează, java.lang.Iterabledar hărțile încorporate sincronizate și alte colecții încorporate nu furnizează iteratori sincronizați, astfel încât sincronizarea este lăsată în sarcina codului client, care este lent și predispus la erori și nu este posibil să ne așteptăm să fie duplicat de către alți consumatori de harta sincronizată. Întreaga durată a iterației trebuie protejată, de asemenea. Mai mult, o hartă care este înfășurată de două ori în locuri diferite va avea diferite obiecte interne mutex pe care operează sincronizările, permițând suprapunerea. Delegația este un reductor de performanță, dar compilatoarele moderne Just-in-Time adesea se aliniază puternic, limitând reducerea performanței. Iată cum funcționează împachetarea în interiorul ambalajului - mutexul este doar un obiect final și m este harta finală înfășurată:

    public V put(K key, V value) {
        synchronized (mutex) {return m.put(key, value);}
    }

Sincronizarea iterației este recomandată după cum urmează, cu toate acestea, aceasta se sincronizează pe ambalaj mai degrabă decât pe mutex intern, permițând suprapunerea:

    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
        }
    }

Sincronizare nativă

Orice hartă poate fi utilizată în siguranță într-un sistem cu mai multe fire, asigurându-se că toate accesele la aceasta sunt gestionate de mecanismul de sincronizare Java:

    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

Codul care utilizează un java.util.concurrent.ReentrantReadWriteLock este similar cu cel pentru sincronizarea nativă. Cu toate acestea, pentru siguranță, încuietorile ar trebui să fie utilizate într-un bloc de încercare / în cele din urmă, astfel încât ieșirea timpurie, cum ar fi Aruncarea excepției sau Pauza / continuarea, să treacă cu siguranță prin deblocare. Această tehnică este mai bună decât utilizarea sincronizării, deoarece citirile se pot suprapune, există o nouă problemă în a decide cum să prioritizați scrierile în raport cu citirile. Pentru simplitate, se poate utiliza în schimb un java.util.concurrent.ReentrantLock, care nu face distincție de citire / scriere. Mai multe operații pe încuietori sunt posibile decât cu sincronizare, cum ar fi tryLock() și 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();
    }

Convoaiele

Excluderea reciprocă are o problemă a convoiului de blocare , în care firele se pot acumula pe o încuietoare, determinând JVM să aibă nevoie să mențină cozi scumpe de chelneri și să „parcheze” firele de așteptare. Este scump să parcați și să dezarhivați un fir și poate apărea un comutator lent de context. Comutatoarele de context necesită de la microsecunde la milisecunde, în timp ce operațiunile de bază ale Hărții durează în mod normal nanosecunde. Performanța poate scădea la o mică fracțiune din randamentul unui singur fir, pe măsură ce disputa crește. Când nu există sau există puține dispute pentru încuietoare, există un impact redus asupra performanței, cu excepția testului de menținere a încuietorii. JVM-urile moderne vor integra cea mai mare parte a codului de blocare, reducându-l la doar câteva instrucțiuni, menținând foarte rapid carcasa fără contestație. Tehnicile de reintroducere, cum ar fi sincronizarea nativă sau java.util.concurrent.ReentrantReadWriteLock, au totuși un bagaj suplimentar de reducere a performanței în menținerea adâncimii de reintrare, afectând și cazul fără contestație. Problema convoiului pare să se ușureze cu JVMS-ul modern, dar poate fi ascunsă prin comutarea lentă a contextului: în acest caz, latența va crește, dar randamentul va continua să fie acceptabil. Cu sute de fire, un timp de comutare a contextului de 10 ms produce o latență în secunde.

Multe nuclee

Soluțiile de excludere reciprocă nu reușesc să profite de toată puterea de calcul a unui sistem cu mai multe nuclee, deoarece un singur fir este permis în interiorul codului Hărții la un moment dat. Implementările hărților concurente speciale furnizate de Java Collections Framework și altele profită uneori de mai multe nuclee folosind tehnici de programare fără blocare . Tehnicile fără blocare utilizează operațiuni precum metoda intrinsecă compareAndSet () disponibilă pe multe dintre clasele Java, cum ar fi AtomicReference, pentru a face actualizări condiționale ale unor structuri interne ale hărții în mod atomic. Primitivul compareAndSet () este mărit în clasele JCF de codul nativ care poate face compareAndSet pe părți interne speciale ale unor obiecte pentru unii algoritmi (folosind accesul „nesigur”). Tehnicile sunt complexe, bazându-se adesea pe regulile comunicării inter-thread furnizate de variabilele volatile, relația care se întâmplă înainte, tipuri speciale de „bucle de reîncercare” fără blocare (care nu sunt ca blocările de rotire în sensul că produc întotdeauna progrese) . CompareAndSet () se bazează pe instrucțiuni speciale specifice procesorului. Este posibil ca orice cod Java să utilizeze în alte scopuri metoda compareAndSet () pe diferite clase concurente pentru a obține concurență fără blocare sau chiar fără așteptare, care oferă o latență finită. Tehnicile fără blocare sunt simple în multe cazuri obișnuite și cu unele colecții simple, cum ar fi stive.

Diagrama indică modul în care sincronizarea utilizând Collections.synchronizedMap () împachetarea unui HashMap obișnuit (violet) nu poate scala, precum și ConcurrentHashMap (roșu). Celelalte sunt comandate ConcurrentNavigableMaps AirConcurrentMap (albastru) și ConcurrentSkipListMap (verde CSLM). (Punctele plate pot fi rehashes producând tabele care sunt mai mari decât pepinieră, iar ConcurrentHashMap ocupă mai mult spațiu. Notă că axa y ar trebui să spună „pune K”. Sistemul are 8 nuclee i7 2,5 GHz, cu -Xms5000m pentru a preveni GC). Extinderea proceselor GC și JVM schimbă curbele considerabil, iar unele tehnici interne fără blocare generează gunoi în condiții de dispută.

Tabelele hash sunt ambele rapide

Numai Hărțile comandate se redimensionează, iar Harta sincronizată se retrage Harta sincronizată a revenit pentru a fi similară cu Hărțile comandate la scară

Latență previzibilă

O altă problemă cu abordările de excludere reciprocă este că presupunerea de atomicitate completă făcută de un cod cu un singur fir creează întârzieri sporadice inacceptabil de lungi între fire într-un mediu concurent. În special, Iteratorii și operațiunile în bloc, cum ar fi putAll () și altele, pot lua o perioadă de timp proporțională cu dimensiunea hărții, amânând alte fire care se așteaptă cu o latență scăzută în mod previzibil pentru operațiunile non-bulk. De exemplu, un server web cu mai multe fire nu poate permite ca unele răspunsuri să fie întârziate prin iterații de lungă durată ale altor fire executând alte cereri care caută o anumită valoare. Legat de acest lucru este faptul că firele care blochează harta nu au de fapt nicio cerință de renunțare la blocare, iar o buclă infinită din firul proprietar poate propaga blocarea permanentă către alte fire. Firele cu proprietar lent pot fi uneori întrerupte. Hărțile bazate pe Hash sunt, de asemenea, supuse unor întârzieri spontane în timpul rehahing-ului.

Consistență slabă

Soluția pachetelor java.util.concurrency la problema de modificare concurentă, problema convoiului, problema de latență previzibilă și problema multi-core include o alegere arhitecturală numită consistență slabă. Această alegere înseamnă că citirile precum get () nu se vor bloca chiar și atunci când actualizările sunt în curs și este permisă chiar și ca actualizările să se suprapună cu ele însele și cu citirile. Consistența slabă permite, de exemplu, conținutul unui ConcurrentMap să se schimbe în timpul unei iterații a acestuia printr-un singur fir. Iteratoarele sunt proiectate pentru a fi utilizate de câte un fir la un moment dat. De exemplu, o hartă care conține două intrări care sunt interdependente poate fi văzută într-un mod inconsecvent de un fir cititor în timpul modificării de către un alt fir. O actualizare care ar trebui să schimbe atomic cheia unei intrări ( k1, v ) într-o intrare ( k2, v ) ar trebui să facă o eliminare ( k1 ) și apoi un put ( k2, v ), în timp ce o iterație ar putea lipsi intrarea sau vedeți-o în două locuri. Preluările returnează valoarea unei chei date care reflectă cea mai recentă actualizare finalizată anterioară pentru acea cheie. Astfel există o relație „se întâmplă înainte”.

Nu există nicio modalitate prin care ConcurrentMaps să blocheze întregul tabel. Nu există nicio posibilitate de ExcepțieModificareConcurențială, așa cum există și cu modificări concurente involuntare ale Hărților neconcurente. Metoda size () poate dura mult timp, spre deosebire de hărțile corespunzătoare non-concurente și alte colecții care includ de obicei un câmp de dimensiuni pentru acces rapid, deoarece este posibil să fie nevoie să scaneze întreaga hartă într-un fel. Când apar modificări concurente, rezultatele reflectă starea Hărții la un moment dat, dar nu neapărat o singură stare consecventă, prin urmare, dimensiunea (), isEmpty () și containValue () pot fi utilizate cel mai bine doar pentru monitorizare.

Metode ConcurrentMap 1.5

Există câteva operații furnizate de ConcurrentMap care nu se află pe hartă - pe care le extinde - pentru a permite atomicitatea modificărilor. Înlocuirea ( K, v1, v2 ) va testa existența v1 în intrarea identificată de K și numai dacă este găsită, atunci v1 este înlocuit cu v2 atomic. Noul înlocuitor ( k, v ) va face un put ( k, v ) numai dacă k este deja în hartă. De asemenea, putIfAbsent ( k, v ) va face un put ( k, v ) numai dacă k nu este deja pe hartă, iar eliminarea (k, v) va elimina Intrarea pentru v numai dacă v este prezent. Această atomicitate poate fi importantă pentru unele cazuri de utilizare cu mai multe fire, dar nu este legată de constrângerea de consistență slabă.

Pentru ConcurrentMaps, următoarele sunt atomice.

m.putIfAbsent (k, v) este atomic, dar echivalent cu:

    if (k == null || v == null)
            throw new NullPointerException();
    if (!m.containsKey(k)) {
        return m.put(k, v);
    } else {
        return m.get(k);
    }

m replace (k, v) este atomic dar echivalent cu:

    if (k == null || v == null)
            throw new NullPointerException();
    if (m.containsKey(k)) {
        return m.put(k, v);
    } else {
        return null;
    }

m.replace (k, v1, v2) este atomic dar echivalent cu:

    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) este atomic, dar echivalent cu:

    // 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;
    }

Metode ConcurrentMap 1.8

Deoarece Map și ConcurrentMap sunt interfețe, nu pot fi adăugate noi metode fără a rupe implementările. Cu toate acestea, Java 1.8 a adăugat capacitatea de implementare implicită a interfeței și a adăugat implementărilor implicite ale interfeței Map a unor noi metode getOrDefault (Object, V), forEach (BiConsumer), replaceAll (BiFunction), computeIfAbsent (K, Function), computeIfPresent ( K, BiFunction), calculează (K, BiFunction) și îmbină (K, V, BiFunction). Implementările implicite din Harta nu garantează atomicitatea, dar în valorile implicite supranumite ConcurrentMap, acestea folosesc tehnici fără blocare pentru a atinge atomicitatea, iar implementările existente ConcurrentMap vor fi automat atomice. Tehnicile fără blocare pot fi mai lente decât suprascrierile din clasele de beton, astfel încât clasele de beton pot alege să le implementeze atomic sau nu și să documenteze proprietățile concurenței.

Atomicitate fără blocare

Este posibil să se utilizeze tehnici fără blocare cu ConcurrentMaps, deoarece acestea includ metode cu un număr suficient de mare de consens, și anume infinit, ceea ce înseamnă că orice număr de fire poate fi coordonat. Acest exemplu ar putea fi implementat cu Java 8 merge (), dar arată modelul general fără blocare, care este mai general. Acest exemplu nu este legat de internele ConcurrentMap, ci de utilizarea codului clientului de ConcurrentMap. De exemplu, dacă vrem să înmulțim atomic o valoare din hartă cu o constantă C:

    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;
        }
    }

PutIfAbsent ( k, v ) este, de asemenea, util atunci când este permisă absența intrării pentru cheie. Acest exemplu ar putea fi implementat cu calculul Java 8 (), dar arată modelul general fără blocare, care este mai general. Înlocuirea ( k, v1, v2 ) nu acceptă parametri nuli, deci uneori este necesară o combinație a acestora. Cu alte cuvinte, dacă v1 este nul, atunci se invocă putIfAbsent ( k, v2 ), altfel se invocă înlocuirea ( k, v1, v2 ).

    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);
    }

Istorie

Cadrul colecțiilor Java a fost conceput și dezvoltat în principal de Joshua Bloch și a fost introdus în JDK 1.2 . Clasele inițiale de concurență provin din pachetul de colectare al lui Doug Lea .

Vezi si

Referințe

  • Goetz, Brian; Joshua Bloch; Joseph Bowbeer; Doug Lea; David Holmes; Tim Peierls (2006). Java simultaneitate în practică . Addison Wesley. ISBN 0-321-34960-1. OL  25208908M .
  • Lea, Doug (1999). Programare simultană în Java: Principii și modele de proiectare . Addison Wesley. ISBN 0-201-31009-0. OL  55044M .

linkuri externe