Try using it in your preferred language.

English

  • English
  • 汉语
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar
translation

Questo è un post tradotto da IA.

제이온

[Java] Synchronized Collection vs Concurrent Collection

Seleziona la lingua

  • Italiano
  • English
  • 汉语
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar

Testo riassunto dall'intelligenza artificiale durumis

  • Synchronized collections like Vector, Hashtable, Collections.synchronizedXXX internally use the synchronized keyword to ensure concurrency, but they may have problems with performance degradation or when using multiple operations together.
  • The concurrent collections provided in the java.util.concurrent package offer various classes like CopyOnWriteArrayList, ConcurrentMap, ConcurrentHashMap, and are more performant than synchronized collections.
  • Concurrent collections like CopyOnWriteArrayList, ConcurrentHashMap have better read performance than synchronized collections, and it's more effective to use them when write operations are relatively infrequent.

Collezione sincronizzata

Le collezioni sincronizzate hanno tipicamente le seguenti classi:


  • Vettore
  • Hashtable
  • Collections.synchronizedXXX


Tutte queste classi garantiscono la concorrenza usando la parola chiave synchronized nei metodi dichiarati pubblicamente, impedendo così che più thread contemporaneamente accedano ai valori interni.


Vettore

public class Vector extends AbstractList
    implements List, RandomAccess, Cloneable, java.io.Serializable {
    ...
    public synchronized boolean add(E e) {
                modCount++;
                add(e, elementData, elementCount);
                return true;
    }
    ...

Se si osserva il metodo add() della classe Vector, che si occupa di aggiungere un elemento, si nota la parola chiave synchronized. Ciò significa che all'interno di Vector, l'operazione di inserimento di un elemento è garantita concorrenza.

Hashtable

public class Hashtable extends Dictionary
    implements Map, Cloneable, java.io.Serializable {
        ...
        public synchronized boolean contains(Object value) {
        if (value == null) {
            throw new NullPointerException();
        }

        Entry tab[] = table;
        for (int i = tab.length ; i-- > 0 ;) {
            for (Entry e = tab[i] ; e != null ; e = e.next) {
                if (e.value.equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }
        ...


Nel metodo contains() della classe Hashtable, che verifica se esiste un valore identico, si può osservare, come nella classe Vector, l'utilizzo della parola chiave synchronized.


Collections.synchronizedXXX

Diamo un'occhiata alla classe SynchronizedList, creata usando il metodo Collections.synchronizedList().


static class SynchronizedList extends SynchronizedCollection
        implements List {

        final Object mutex;

        ...
        public E get(int index) {
                synchronized (mutex) {return list.get(index);}
        }

        public E set(int index, E element) {
                synchronized (mutex) {return list.set(index, element);}
        }

        public void add(int index, E element) {
                synchronized (mutex) {list.add(index, element);}
        }

        public E remove(int index) {
                synchronized (mutex) {return list.remove(index);}
        }
        ...


Osservando i metodi della classe SynchronizedList, si nota che tutti usano la parola chiave synchronized. Tuttavia, la concorrenza è implementata usando un mutex tramite un blocco synchronized. Tutti i metodi condividono l'oggetto mutex, quindi quando un thread entra nel blocco synchronized di un metodo, tutti gli altri blocchi synchronized degli altri metodi vengono bloccati.


Problemi della collezione sincronizzata

In un ambiente multi-thread, può essere necessario usare collezioni sincronizzate, ma è meglio usare altri metodi di concorrenza, se possibile. Ci sono due motivi principali per questo.


Combinazione di più operazioni come un'unica operazione

Le classi di collezioni sincronizzate garantiscono la concorrenza in un ambiente multi-thread. Tuttavia, se è necessario combinare più operazioni in un'unica operazione, potrebbero sorgere problemi. L'uso di queste collezioni sincronizzate potrebbe non garantire un corretto funzionamento.


final List list = Collections.synchronizedList(new ArrayList());
final int nThreads = 2;
ExecutorService es = Executors.newFixedThreadPool(nThreads);

for (int i = 0; i < nThreads; i++) {
    es.execute(new Runnable() {

        public void run() {
            while(true) {
                try {
                    list.clear();
                    list.add("888");
                    list.remove(0);
                } catch(IndexOutOfBoundsException e) {
                    e.printStackTrace();
                }
            }
        }
    });


Se si esegue il codice sopra, se il Thread A esegue remove(0) mentre il Thread B esegue clear(), si verifica un errore. Pertanto, è necessario raggruppare le operazioni in un blocco synchronized come mostrato di seguito.


synchronized (list) {
    list.clear();
    list.add("888");
    list.remove(0);


Riduzione delle prestazioni

Se si trasformano tutti i metodi che tentano di accedere all'oggetto condiviso in metodi synchronized o se si definiscono blocchi synchronized identici all'interno dei metodi, quando un thread acquisisce il lock, tutti gli altri thread non possono utilizzare alcun metodo sincronizzato e finiscono in stato di blocco. Questo fenomeno ripetuto può portare a una riduzione delle prestazioni.


Collezione concorrente

Il pacchetto java.util.concurrent fornisce diversi tipi di collezioni parallele, di cui alcune saranno trattate in questo articolo.


  • CopyOnWriteArrayList
    • È una sottoclasse della classe List e una collezione parallela che privilegia le prestazioni delle operazioni di lettura e iterazione sugli elementi dell'elenco.
  • ConcurrentMap
    • È una collezione parallela. L'interfaccia include operazioni come put-if-absent, replace e rimuovi-condizionale, che consentono di aggiungere elementi solo se non esistono già.
  • ConcurrentHashMap
    • È una sottoclasse di ConcurrentMap e una collezione parallela che sostituisce HashMap garantendo la concorrenza.
  • ConcurrentLinkedQueue
    • È una coda FIFO che garantisce la concorrenza. Se non ci sono elementi da estrarre dalla coda, restituisce immediatamente un valore e continua a eseguire altre operazioni.
  • LinkedBlockingQueue
    • È simile a ConcurrentLinkedQueue, ma se la coda è vuota, l'operazione di estrazione di un elemento dalla coda attende che venga aggiunto un nuovo elemento. Al contrario, se la coda ha una dimensione definita, l'operazione di aggiunta di un nuovo elemento alla coda attende che si liberi uno spazio nella coda.
  • ConcurrentSkipListMap, ConcurrentSkipListSet
    • Sono forme avanzate di SortedMap e SortedSet che migliorano la concorrenza.


Sostituire le tradizionali classi di collezioni sincronizzate con le collezioni parallele può aumentare notevolmente le prestazioni generali senza introdurre rischi aggiuntivi.

Analizziamo più in dettaglio le collezioni sincronizzate e le collezioni parallele, evidenziandone le differenze.


CopyOnWriteArrayList

Esistono due metodi per creare un ArrayList sincronizzato:


  • Collections.synchronizedList()
  • CopyOnWriteArrayList


Collections.synchronizedList() è stato aggiunto in JDK 1.2. Questa collezione sincronizza tutte le operazioni di lettura e scrittura, il che la rende un design poco flessibile. Per ovviare a questo problema, è stato introdotto CopyOnWriteArrayList.


Operazioni di lettura

SynchronizedList blocca l'istanza stessa sia per le operazioni di lettura che di scrittura. Tuttavia, CopyOnWriteArrayList crea un nuovo array temporaneo copiando gli elementi dall'array originale per ogni operazione di scrittura, esegue l'operazione di scrittura sull'array temporaneo e quindi aggiorna l'array originale. Grazie a questo, le operazioni di lettura non sono bloccate, quindi le prestazioni sono migliori rispetto a SynchronizedList.


public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {

    final transient Object lock = new Object();
    private transient volatile Object[] array;

        ...
        public E get(int index) {
        return elementAt(getArray(), index);
    }
        ...


Il codice sopra mostra il metodo get(), che non è sincronizzato, quindi non è bloccato.


Operazioni di scrittura

CopyOnWriteArrayList utilizza un lock esplicito durante l'esecuzione delle operazioni di scrittura. In definitiva, entrambe le collezioni vengono bloccate durante questa operazione. Tuttavia, CopyOnWriteArrayList esegue un'operazione di copia dell'array, che è relativamente costosa, quindi se vengono eseguite molte operazioni di scrittura, potrebbero verificarsi problemi di prestazioni.


public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {

    final transient Object lock = new Object();
    private transient volatile Object[] array;

        public void add(int index, E element) {
        synchronized (lock) {
            ...
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(es, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(es, 0, newElements, 0, index);
                System.arraycopy(es, index, newElements, index + 1,
                                 numMoved);
            }
                        ...
        }
    }


Il codice sopra mostra il metodo add(), che utilizza un blocco synchronized per bloccare l'operazione e eseguire la copia dell'array.


Iteratore

In CopyOnWriteArrayList, l'iteratore viene estratto in base ai dati della collezione al momento dell'estrazione e l'iterazione viene eseguita usando una copia. Pertanto, non ci sono problemi di concorrenza, anche se vengono aggiunti o eliminati dati dalla collezione durante l'iterazione.


CopyOnWriteArraySet

Esistono due metodi per creare un Set sincronizzato:


  • Collections.synchronizedSet()
  • CopyOnWriteArraySet


Come suggerisce il nome del metodo, il funzionamento di CopyOnWriteArraySet è quasi identico a quello di CopyOnWriteArrayList, ad eccezione delle caratteristiche della struttura dei dati.


Operazioni di lettura

public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {

    private final CopyOnWriteArrayList al;

        public boolean contains(Object o) {
        return al.contains(o);
    }


Il codice sopra mostra il metodo contains(). Si può vedere che CopyOnWriteArraySet definisce internamente CopyOnWriteArrayList e utilizza i suoi metodi.


public boolean contains(Object o) {
        return indexOf(o) >= 0;
}

public int indexOf(Object o) {
        Object[] es = getArray();
        return indexOfRange(o, es, 0, es.length);
}

    private static int indexOfRange(Object o, Object[] es, int from, int to) {
        if (o == null) {
            for (int i = from; i < to; i++)
                if (es[i] == null)
                    return i;
        } else {
            for (int i = from; i < to; i++)
                if (o.equals(es[i]))
                    return i;
        }
        return -1;


Il metodo contains() di CopyOnWriteArrayList non è bloccato.


Operazioni di scrittura

public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {

        private final CopyOnWriteArrayList al;

        public boolean add(E e) {
        return al.addIfAbsent(e);
    }


Il metodo add() utilizza i metodi di CopyOnWriteArrayList.


public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOfRange(e, snapshot, 0, snapshot.length) < 0
        && addIfAbsent(e, snapshot);
}

private boolean addIfAbsent(E e, Object[] snapshot) {
        synchronized (lock) {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i]
                        && Objects.equals(e, current[i]))
                        return false;
                if (indexOfRange(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        }


Il metodo addIfAbsent() mostra che l'operazione di scrittura blocca l'operazione e copia l'array. Pertanto, come CopyOnWriteArrayList, è meglio evitare di eseguire troppe operazioni di scrittura su CopyOnWriteArraySet.


ConcurrentHashMap

Esistono due metodi per creare un HashMap sincronizzato:


  • Collections.synchronizedMap(new HashMap<>())
  • ConcurrentHashMap


ConcurrentHashMap è una mappa basata su hash, come HashMap. È più efficiente di synchronizedMap nel garantire la concorrenza.


Prima di Java 8, veniva utilizzato Segment, che eredita da ReentrantLock, per separare le aree e applicare il lock a ciascuna di esse.


Da Java 8 in poi, viene utilizzato un metodo che blocca ogni bucket della tabella in modo indipendente. Se viene inserito un nodo in un bucket vuoto, viene utilizzato l'algoritmo CAS invece del lock. Per le altre modifiche, viene acquisito un lock parziale (blocco synchronized) dal primo nodo del bucket per ridurre al minimo la concorrenza tra i thread e garantire la concorrenza.


Diamo un'occhiata al codice del metodo putVal() di ConcurrentHashMap, che inserisce un nuovo nodo, per capire come garantisce la concorrenza. Il codice di esempio seguente si basa su Java 11.


Il metodo putVal() può essere suddiviso in due casi principali (con un totale di quattro rami):


  • Inserimento di un nodo in un bucket hash vuoto
  • Se il bucket hash contiene già un nodo


final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node[] tab = table;;) {
            Node f; int n, i, fh; K fk; V fv;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
                        // (1)
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                                // (2)
                if (casTabAt(tab, i, null, new Node(hash, key, value)))
                    break;
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
                        // (3)
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node e = f;; ++binCount) {
                                K ek;
                                                                // (4)
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node pred = e;
                                                                // (5)
                                if ((e = e.next) == null) {
                                    pred.next = new Node(hash, key, value);
                                    break;
                                }
                            }
                        }
                                                // (6)
                        else if (f instanceof TreeBin) {
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                                ...
            }
        }
                ...


Inserimento di un nodo in un bucket hash vuoto

(1) Per inserire un nuovo nodo, viene controllato se il valore del bucket corrispondente (tabAt()) è vuoto.


static final  Node tabAt(Node[] tab, int i) {
        return (Node)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);


(2) Viene controllato se il valore della variabile volatile presente nel nodo è uguale al valore esistente (null) e, in caso affermativo, viene memorizzato un nuovo nodo. Se non è uguale, il ciclo for viene ripetuto. Questo metodo si chiama algoritmo CAS.


static final  boolean casTabAt(Node[] tab, int i, Node c, Node v) {
        return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);


L'algoritmo CAS risolve i problemi di atomicità e visibilità, garantendo la concorrenza.


Se il bucket hash contiene già un nodo

(3) Se esiste già un nodo, viene utilizzato un blocco synchronized per consentire l'accesso a un solo thread. In questo caso, viene applicato un lock al bucket hash non vuoto di tipo Node, quindi i thread che accedono allo stesso bucket vengono bloccati.

(4) Il nodo viene sostituito con un nuovo nodo.

(5) Se si verifica una collisione di hash, viene aggiunto a Separate Chaing.

(6) Se si verifica una collisione di hash, viene aggiunto a un albero.


Riferimenti


Domande e risposte del colloquio

Quali sono i problemi di Vector, HashTable e Collections.SynchronziedXXX?

Le classi Vector, HashTable e SynchronziedXxx usano metodi o blocchi synchronized e condividono uno stesso lock. Quindi, quando un thread acquisisce il lock sulla collezione, tutti gli altri thread non possono usare alcun metodo e finiscono in stato di blocco. Questo può portare a una riduzione delle prestazioni dell'applicazione.


Qual è la differenza tra SynchronizedList e CopyOnArrayList?

SynchronizedList blocca l'istanza stessa sia per le operazioni di lettura che di scrittura. Tuttavia, CopyOnArrayList blocca il blocco durante l'operazione di scrittura, copia gli elementi dall'array originale per creare un nuovo array temporaneo, esegue l'operazione di scrittura sull'array temporaneo e quindi aggiorna l'array originale. Grazie a questo, le operazioni di lettura non sono bloccate, quindi le prestazioni di lettura sono migliori rispetto a SynchronizedList. Tuttavia, l'operazione di scrittura comporta un'operazione di copia dell'array, che è costosa, quindi le prestazioni di scrittura sono inferiori a quelle di SynchronizedList.

Pertanto, è più efficiente usare CopyOnArrayList se si hanno più operazioni di lettura che di modifica.


Qual è la differenza tra SynchronizedMap e ConcurrentHashMap?

SynchronziedMap blocca l'istanza stessa sia per le operazioni di lettura che di scrittura. Tuttavia, ConcurrentHashMap utilizza un metodo che blocca ogni bucket della tabella in modo indipendente. Se viene inserito un nodo in un bucket vuoto, viene utilizzato l'algoritmo CAS invece del lock. Per le altre modifiche, viene acquisito un lock parziale (blocco synchronized) dal primo nodo del bucket per ridurre al minimo la concorrenza tra i thread e garantire la concorrenza.

제이온
제이온
제이온
제이온
[Spring] Come usare @Async Scopri come implementare facilmente la gestione asincrona di Java usando Spring @Async. Impara come convertire i metodi sincroni in asincroni tramite l'annotazione @Async e come migliorare l'efficienza impostando un pool di thread. Verrà anche trattato co

25 aprile 2024

[Effictive Java] Item 6. Evitare la creazione di oggetti non necessari Questa è una guida su come ridurre la creazione di oggetti non necessari in Java. Per gli oggetti immutabili come String e Boolean, è meglio usare i letterali e per le espressioni regolari è meglio mettere in cache l'istanza di Pattern. Inoltre, l'autobox

28 aprile 2024

Che cos'è Java Collections Framework (JCF)? - Definizione e caratteristiche di JCF (JAVA) Java Collections Framework (JCF) è un insieme di classi Java che fornisce un modo standardizzato per elaborare in modo efficiente grandi quantità di dati. JCF implementa strutture di dati di archiviazione e algoritmi come classi per migliorare la riutiliz

27 aprile 2024

[Concurrency] Operazione Atomica: Memory Fence e Memory Ordering Questo post del blog spiega come considerare l'ordine di memoria nelle operazioni atomiche e l'importanza delle opzioni di ordinamento. Vengono spiegate le diverse opzioni di ordinamento come Relaxed, Acquire, Release, AcqRel, SecCst, insieme ai vantaggi
곽경직
곽경직
곽경직
곽경직
곽경직

12 aprile 2024

Come Rust impedisce i bug di concorrenza Rust è un linguaggio potente che affronta le sfide della programmazione concorrente. Il suo sistema di tipi e il modello di proprietà garantiscono la sicurezza nella condivisione e nel trasferimento di dati tra thread. Tramite pattern di mutabilità intern
곽경직
곽경직
곽경직
곽경직
곽경직

28 marzo 2024

[Non specialisti, sopravvivere come sviluppatori] 14. Riepilogo dei contenuti del colloquio tecnico per sviluppatori junior Questa è una guida alla preparazione ai colloqui tecnici per sviluppatori junior. Copre argomenti come la memoria principale, le strutture dati, RDBMS e NoSQL, programmazione procedurale e orientata agli oggetti, override e overload, algoritmi di sostituz
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자

3 aprile 2024

[Javascript] Struttura dell'oggetto (V8) L'oggetto JavaScript in V8 Engine viene ottimizzato come una struttura in base allo stato e convertito in modalità Fast e modalità Dictionary che funziona come una hashmap. La modalità Fast è rapida con chiave e valore in un formato quasi fisso, ma se vie
곽경직
곽경직
곽경직
곽경직
곽경직

18 marzo 2024

Modellazione dati fisica La modellazione dati fisica è il processo di progettazione delle tabelle di un database relazionale per renderle effettivamente utilizzabili, mirando all'ottimizzazione delle prestazioni attraverso l'efficienza dello spazio di archiviazione, il partiziona
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

9 aprile 2024

Modellazione logica dei dati La modellazione logica dei dati è il processo di conversione di un modello di dati concettuale in un modello di database relazionale secondo il paradigma del database relazionale, gestendo relazioni 1:1, 1:N, N:M e garantendo l'integrità dei dati attraver
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

9 aprile 2024