Esiste un elenco simultaneo nel JDK di Java?


287

Come posso creare un'istanza di List simultanea, dove posso accedere agli elementi per indice? Il JDK ha classi o metodi di fabbrica che posso usare?


29
Perché non costruttivo? Diversi CopyOnWriteArrayList proposti che non si trovano in .Net. Puoi dire che entrambe le domande si riferiscono l'una all'altra ma non chiudere questa !!!
AlikElzin-kilaka

1
Non ho idea del motivo per cui Jarrod Roberson avrebbe pensato che fosse una buona idea prendere le modifiche dettagliate apportate da Stephan e riportarle alla domanda originale, mal formulata. La risposta di Jarrod è ancora perfettamente accettabile. In effetti, CopyOnWriteArrayList è l'unica classe concorrente che implementa List nel JDK. Perplesso ...
Matt Passell

11
Perché la risposta accettata era alla domanda originale e Stephan ha posto una domanda completamente non correlata con un mucchio di codice sorgente che il poster originale non includeva da nessuna parte cambiando completamente la domanda, il che ha generato più risposte che suggerivano cose diverse da quelle Listspecificatamente originali dice è un requisito che è considerato vandalismo. Un moderatore ha già bloccato la domanda a causa delle persone che si lamentano del fatto che le risposte non rispondono a quella versione vandalizzata della domanda.

1
/ locked/ closed/ Precedente commento

9
Non c'è motivo per chiudere questa domanda. Chiede delle classi nel JDK, che non è niente come cercare una libreria; è la base di Java.
maaartinus

Risposte:


181

Esiste un'implementazione dell'elenco simultaneo in java.util.concurrent . CopyOnWriteArrayList in particolare.


85
Nota che copia l'intero elenco su ogni inserto, quindi è spesso inefficiente.
dfrankow

23
@dfrankow Ma può più più efficiente se si sta scorrendo molto più di quanto si sta aggiornando.
b1nary.atr0phy

Non funziona bene come mostrato qui. Ho delle eccezioni anche se uso semplicemente il suo metodo addAll e lo leggo usando stream. stackoverflow.com/questions/1527519/...
devssh

173

Se non ti interessa avere un accesso basato sull'indice e desideri solo le caratteristiche di conservazione dell'ordine di inserzione di un List, potresti prendere in considerazione java.util.concurrent.ConcurrentLinkedQueue . Poiché implementa Iterable, una volta che hai finito di aggiungere tutti gli elementi, puoi scorrere i contenuti utilizzando la sintassi migliorata:

Queue<String> globalQueue = new ConcurrentLinkedQueue<String>();

//Multiple threads can safely call globalQueue.add()...

for (String href : globalQueue) {
    //do something with href
}

4
Penso che l'istruzione for semplificata ( :) sia chiamata foreach: docs.oracle.com/javase/1.5.0/docs/guide/language/foreach.html
AlikElzin-kilaka

2
@ AlikElzin-kilaka Hai ragione. Penso che quel nome mi abbia sempre infastidito, perché la sintassi effettiva non include la parola "ciascuno", ma aggiornerò la risposta per utilizzare il nome ufficiale. :)
Matt Passell

3
@ AlikElzin-kilaka Nitpicking, ma secondo la versione 8 di JLS è chiamato "Enhanced for Statement". Lo stesso nel tutorial java .
Roland

1
@ Roland sicuramente NON pignolo. C'è (ora) una differenza tra "for each" e "enhanced for" in Java.
hfontanez

3
@ Roland indirettamente. Credo che abbiano rinominato "per ogni ciclo" in "migliorato per" per eliminare la confusione tra Stream.forEach e ciò che ora è noto come potenziato per.
hfontanez

131

Puoi usare molto bene Collections.synchronizedList (List) se tutto ciò di cui hai bisogno è una semplice sincronizzazione delle chiamate:

 List<Object> objList = Collections.synchronizedList(new ArrayList<Object>());

72
Il risultato di synchronizedListè "sincronizzato" ma non "concorrente". Una questione fondamentale è che molte operazioni List, che sono basate su indice, non sono di per sé atomiche e devono essere parte di un più ampio costrutto di mutua esclusione.

6
IMO, asing a Vectorè più semplice piuttosto che Collections.synchronizedList(new ArrayList<Object>()).
Stephan

9
Il risultato di synchronizedList è "sincronizzato" ma non "concorrente".
Kanagavelu Sugumar

46

Perché l'atto di acquisire la posizione e ottenere l'elemento dalla posizione data richiede naturalmente un certo bloccaggio (non puoi fare in modo che l'elenco abbia cambiamenti strutturali tra queste due operazioni).

L'idea stessa di una raccolta simultanea è che ogni operazione da sola è atomica e può essere eseguita senza blocco / sincronizzazione espliciti.

Quindi ottenere l'elemento in posizione nda un dato Listcome un'operazione atomica non ha molto senso in una situazione in cui è previsto l'accesso simultaneo.


7
Joachim, penso che tu abbia colto nel segno. Prendiamo ad esempio un elenco di sola lettura come elenco simultaneo. Ottenere l'elemento nella posizione N dalla lista non solo ha senso, ma è il nocciolo del problema. Quindi, una lista immutabile (L minuscola) sarebbe un buon esempio, ma non è una Lista (L maiuscola). CopyOnWriteArrayList è concorrente, ma a molte persone non piace la performance. Una soluzione sulla falsariga delle corde (corde di corda) sarebbe probabilmente un buon vincitore.
johnstosh

1
Ottimo punto. Ma l'elenco che verrà utilizzato dall'OP potrebbe avere un utilizzo molto specifico. Ad esempio, potrebbe essere compilato in ambiente concorrente, quindi "bloccato" (qualunque cosa significhi) e quindi accessibile in modo sicuro tramite indice. Quindi, nella prima fase di compilazione di tale elenco sarà comunque necessaria un'implementazione thread-safe. Sfortunatamente, l'OP non era specifico su come verrà utilizzata la lista che sta cercando.
igor.zh

15

Hai queste opzioni:

  • Collections.synchronizedList(): Si può avvolgere qualsiasi Listapplicazione ( ArrayList, LinkedListo un elenco 3rd-party). L'accesso a ogni metodo (lettura e scrittura) sarà protetto utilizzando synchronized. Quando si utilizza iterator()o il ciclo for migliorato, è necessario sincronizzare manualmente l'intera iterazione. Durante l'iterazione, gli altri thread sono completamente bloccati anche dalla lettura. Puoi anche sincronizzare separatamente per ciascuna chiamata hasNexte next, ma ConcurrentModificationExceptionè possibile.

  • CopyOnWriteArrayList: è costoso da modificare, ma leggere senza attese. Gli iteratori non lanciano mai ConcurrentModificationException, restituiscono un'istantanea della lista al momento della creazione dell'iteratore anche se la lista viene modificata da un altro thread durante l'iterazione. Utile per elenchi aggiornati di rado. Le operazioni di massa come addAllsono preferite per gli aggiornamenti: l'array interno viene copiato meno volte.

  • Vector: molto simile synchronizedList, ma anche l'iterazione è sincronizzata. Tuttavia, gli iteratori possono generare ConcurrentModificationExceptionse il vettore viene modificato da un altro thread durante l'iterazione.

Altre opzioni:

  • Collections.unmodifiableList(): senza blocco, thread-safe, ma non modificabile
  • Queueo Dequepotrebbe essere un'alternativa se aggiungi / rimuovi solo alla fine dell'elenco e ripeti l'elenco. Non c'è accesso per indice e nessuna aggiunta / rimozione in posti arbitrari. Hanno più implementazioni simultanee con prestazioni migliori e un migliore accesso simultaneo, ma va oltre lo scopo di questa domanda. Puoi anche dare un'occhiata a JCTools , contengono implementazioni di code più performanti specializzate per singolo consumatore o singolo produttore.

4

inserisci qui la descrizione dell'immagine

CopyOnWriteArrayList è una variante thread-safe di ArrayList in cui tutte le operazioni mutative (aggiungi, imposta e così via) vengono implementate creando una nuova copia dell'array sottostante.

CopyOnWriteArrayList è un'alternativa simultanea di List sincronizzato che implementa l'interfaccia List e la sua parte del pacchetto java.util.concurrent ed è una raccolta thread-safe.

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

CopyOnWriteArrayList è a prova di errore e non genera ConcurrentModificationException quando CopyOnWriteArrayList sottostante viene modificato durante l'iterazione usa una copia separata di ArrayList.

Questo è normalmente troppo costoso perché l'array di copie coinvolge ogni operazione di aggiornamento e verrà creata una copia clonata. CopyOnWriteArrayList è la scelta migliore solo per operazioni di lettura frequente.

/**
         * Returns a shallow copy of this list.  (The elements themselves
         * are not copied.)
         *
         * @return a clone of this list
         */
        public Object clone() {
            try {
                @SuppressWarnings("unchecked")
                CopyOnWriteArrayList<E> clone =
                    (CopyOnWriteArrayList<E>) super.clone();
                clone.resetLock();
                return clone;
            } catch (CloneNotSupportedException e) {
                // this shouldn't happen, since we are Cloneable
                throw new InternalError();
            }
        }

0

Se non si prevede mai di eliminare elementi dall'elenco (poiché ciò richiede la modifica dell'indice di tutti gli elementi dopo l'elemento eliminato), è possibile utilizzare ConcurrentSkipListMap<Integer, T>al posto di ArrayList<T>, ad es.

NavigableMap<Integer, T> map = new ConcurrentSkipListMap<>();

Ciò ti consentirà di aggiungere elementi alla fine della "lista" come segue, purché sia ​​presente un solo thread di scrittura (altrimenti c'è una condizione di competizione tra map.size()e map.put()):

// Add item to end of the "list":
map.put(map.size(), item);

Ovviamente puoi anche modificare il valore di qualsiasi elemento nella "lista" (cioè la mappa) semplicemente chiamando map.put(index, item).

Il costo medio per inserire elementi nella mappa o per recuperarli in base all'indice è O (log (n)) ed ConcurrentSkipListMapè privo di blocchi, il che lo rende significativamente migliore di say Vector(la vecchia versione sincronizzata di ArrayList).

È possibile scorrere avanti e indietro l '"elenco" utilizzando i metodi NavigableMapdell'interfaccia.

È possibile racchiudere tutto quanto sopra in una classe che implementa l' Listinterfaccia, purché si comprendano le avvertenze sulle condizioni di competizione (o si possa sincronizzare solo i metodi del writer) e si dovrebbe lanciare un'eccezione di operazione non supportata per i removemetodi. C'è un bel po 'di boilerplate necessario per implementare tutti i metodi richiesti, ma ecco un rapido tentativo di implementazione.

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentSkipListMap;

public class ConcurrentAddOnlyList<V> implements List<V> {
    private NavigableMap<Integer, V> map = new ConcurrentSkipListMap<>();

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return map.values().contains(o);
    }

    @Override
    public Iterator<V> iterator() {
        return map.values().iterator();
    }

    @Override
    public Object[] toArray() {
        return map.values().toArray();
    }

    @Override
    public <T> T[] toArray(T[] a) {
        return map.values().toArray(a);
    }

    @Override
    public V get(int index) {
        return map.get(index);
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return map.values().containsAll(c);
    }

    @Override
    public int indexOf(Object o) {
        for (Entry<Integer, V> ent : map.entrySet()) {
            if (Objects.equals(ent.getValue(), o)) {
                return ent.getKey();
            }
        }
        return -1;
    }

    @Override
    public int lastIndexOf(Object o) {
        for (Entry<Integer, V> ent : map.descendingMap().entrySet()) {
            if (Objects.equals(ent.getValue(), o)) {
                return ent.getKey();
            }
        }
        return -1;
    }

    @Override
    public ListIterator<V> listIterator(int index) {
        return new ListIterator<V>() {
            private int currIdx = 0;

            @Override
            public boolean hasNext() {
                return currIdx < map.size();
            }

            @Override
            public V next() {
                if (currIdx >= map.size()) {
                    throw new IllegalArgumentException(
                            "next() called at end of list");
                }
                return map.get(currIdx++);
            }

            @Override
            public boolean hasPrevious() {
                return currIdx > 0;
            }

            @Override
            public V previous() {
                if (currIdx <= 0) {
                    throw new IllegalArgumentException(
                            "previous() called at beginning of list");
                }
                return map.get(--currIdx);
            }

            @Override
            public int nextIndex() {
                return currIdx + 1;
            }

            @Override
            public int previousIndex() {
                return currIdx - 1;
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }

            @Override
            public void set(V e) {
                // Might change size of map if currIdx == map.size(),
                // so need to synchronize 
                synchronized (map) {
                    map.put(currIdx, e);
                }
            }

            @Override
            public void add(V e) {
                synchronized (map) {
                    // Insertion is not supported except at end of list
                    if (currIdx < map.size()) {
                        throw new UnsupportedOperationException();
                    }
                    map.put(currIdx++, e);
                }
            }
        };
    }

    @Override
    public ListIterator<V> listIterator() {
        return listIterator(0);
    }

    @Override
    public List<V> subList(int fromIndex, int toIndex) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean add(V e) {
        synchronized (map) {
            map.put(map.size(), e);
            return true;
        }
    }

    @Override
    public boolean addAll(Collection<? extends V> c) {
        synchronized (map) {
            for (V val : c) {
                add(val);
            }
            return true;
        }
    }

    @Override
    public V set(int index, V element) {
        synchronized (map) {
            if (index < 0 || index > map.size()) {
                throw new IllegalArgumentException("Index out of range");
            }
            return map.put(index, element);
        }
    }

    @Override
    public void clear() {
        synchronized (map) {
            map.clear();
        }
    }

    @Override
    public synchronized void add(int index, V element) {
        synchronized (map) {
            if (index < map.size()) {
                // Insertion is not supported except at end of list
                throw new UnsupportedOperationException();
            } else if (index < 0 || index > map.size()) {
                throw new IllegalArgumentException("Index out of range");
            }
            // index == map.size()
            add(element);
        }
    }

    @Override
    public synchronized boolean addAll(
            int index, Collection<? extends V> c) {
        synchronized (map) {
            if (index < map.size()) {
                // Insertion is not supported except at end of list
                throw new UnsupportedOperationException();
            } else if (index < 0 || index > map.size()) {
                throw new IllegalArgumentException("Index out of range");
            }
            // index == map.size()
            for (V val : c) {
                add(val);
            }
            return true;
        }
    }

    @Override
    public boolean remove(Object o) {
        throw new UnsupportedOperationException();
    }

    @Override
    public V remove(int index) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        throw new UnsupportedOperationException();
    }
}

Non dimenticare che anche con la sincronizzazione del thread del writer come mostrato sopra, devi fare attenzione a non incappare in condizioni di competizione che potrebbero farti rilasciare elementi, se ad esempio provi a scorrere un elenco in un thread del lettore mentre un il thread del writer viene aggiunto alla fine dell'elenco.

È anche possibile utilizzarlo ConcurrentSkipListMapcome elenco a doppia estremità, a condizione che non sia necessaria la chiave di ciascun elemento per rappresentare la posizione effettiva all'interno dell'elenco (ad esempio, l'aggiunta all'inizio dell'elenco assegnerà agli elementi chiavi negative). (Lo stesso avvertimento sulle condizioni di gara si applica qui, cioè dovrebbe esserci un solo thread di scrittura.)

// Add item after last item in the "list":
map.put(map.isEmpty() ? 0 : map.lastKey() + 1, item);

// Add item before first item in the "list":
map.put(map.isEmpty() ? 0 : map.firstKey() - 1, item);

-1

Principalmente se hai bisogno di un elenco simultaneo si trova all'interno di un oggetto modello (poiché non dovresti usare tipi di dati astratti come un elenco per rappresentare un nodo in un grafico del modello dell'applicazione) o fa parte di un particolare servizio, puoi sincronizzare l'accesso tu stesso .

class MyClass {
  List<MyType> myConcurrentList = new ArrayList<>();
  void myMethod() {
    synchronzied(myConcurrentList) {
      doSomethingWithList;
    }
  }
}

Spesso questo è sufficiente per farti andare avanti. Se è necessario iterare, iterare su una copia dell'elenco e non sull'elenco stesso e sincronizzare solo la parte in cui si copia l'elenco non mentre si sta iterando su di esso.

Inoltre, quando si lavora contemporaneamente su un elenco di solito si fa qualcosa di più che aggiungere, rimuovere o copiare, il che significa che l'operazione diventa abbastanza significativa da giustificare il proprio metodo e l'elenco diventa membro di una classe speciale che rappresenta solo questo particolare elenco con un comportamento thread-safe .

Anche se sono d'accordo sul fatto che sia necessaria un'implementazione di lista simultanea e Vector / Collections.sychronizeList (list) non fa il trucco perché sicuramente hai bisogno di qualcosa come compareAndAdd o compareAndRemove o ottieni (..., ifAbsentDo), anche se hai un Gli sviluppatori di implementazione di ConcurrentList spesso introducono bug non considerando quale sia la vera transazione quando si lavora con elenchi (e mappe) simultanei.

Questi scenari in cui le transazioni sono troppo piccole per lo scopo previsto dell'interazione con un ADT simultaneo (tipo di dati astratto) mi portano sempre a nascondere l'elenco in una classe speciale e sincronizzare l'accesso a questo metodo di oggetti di classe utilizzando il metodo di sincronizzazione sul metodo livello. È l'unico modo per essere sicuri che le transazioni siano corrette.

Ho visto troppi bug per farlo in altro modo - almeno se il codice è importante e gestisce qualcosa come denaro o sicurezza o garantisce alcune misure di qualità del servizio (ad esempio, inviare messaggi almeno una volta e solo una volta).


Blocchi sincronizzati in modo che solo 1 thread possa accedere. Concorrente significa accesso a più thread con blocchi minimi.
AlikElzin-kilaka il

Il primo blocco minimo è una nozione arbitraria. All'interno di un blocco sincronizzato il thread ha accesso esclusivo a una determinata risorsa. Ma l'osservazione di più processi dall'esterno porterebbe alla conclusione che più thread / "processi" potrebbero accedere alla stessa risorsa (elenco simultaneo) "contemporaneamente" ma in modo thread-safe. Ad esempio, un thread aggiunge 100 elementi uno per uno mentre un altro thread accede allo stesso elenco e lo copia quando l'elenco contiene 50 elementi. Questo viene chiamato accesso simultaneo o simultaneo della risorsa poiché entrambi i thread accedono alla stessa risorsa.
Martin Kersten
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.