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()
):
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 NavigableMap
dell'interfaccia.
È possibile racchiudere tutto quanto sopra in una classe che implementa l' List
interfaccia, 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 remove
metodi. 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) {
synchronized (map) {
map.put(currIdx, e);
}
}
@Override
public void add(V e) {
synchronized (map) {
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) {
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()) {
throw new UnsupportedOperationException();
} else if (index < 0 || index > map.size()) {
throw new IllegalArgumentException("Index out of range");
}
add(element);
}
}
@Override
public synchronized boolean addAll(
int index, Collection<? extends V> c) {
synchronized (map) {
if (index < map.size()) {
throw new UnsupportedOperationException();
} else if (index < 0 || index > map.size()) {
throw new IllegalArgumentException("Index out of range");
}
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 ConcurrentSkipListMap
come 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.)
map.put(map.isEmpty() ? 0 : map.lastKey() + 1, item);
map.put(map.isEmpty() ? 0 : map.firstKey() - 1, item);