Un'introduzione alle raccolte Java sincronizzate

1. Panoramica

Il framework delle collezioni è un componente chiave di Java. Fornisce un ampio numero di interfacce e implementazioni, che ci consente di creare e manipolare diversi tipi di raccolte in modo semplice.

Sebbene l'utilizzo di semplici raccolte non sincronizzate sia complessivamente semplice, può anche diventare un processo scoraggiante e soggetto a errori quando si lavora in ambienti multi-thread (ovvero programmazione concorrente).

Pertanto, la piattaforma Java fornisce un forte supporto per questo scenario attraverso diversi wrapper di sincronizzazione implementati nella classe Collections .

Questi wrapper facilitano la creazione di viste sincronizzate delle raccolte fornite mediante diversi metodi di fabbrica statici.

In questo tutorial, approfondiremo questi wrapper di sincronizzazione statica. Inoltre, evidenzieremo la differenza tra raccolte sincronizzate e raccolte simultanee .

2. Il metodo synchronizedCollection ()

Il primo wrapper di sincronizzazione che tratteremo in questo riepilogo è il metodo synchronizedCollection () . Come suggerisce il nome, restituisce una raccolta thread-safe di cui viene eseguito il backup dalla raccolta specificata .

Ora, per capire più chiaramente come utilizzare questo metodo, creiamo uno unit test di base:

Collection syncCollection = Collections.synchronizedCollection(new ArrayList()); Runnable listOperations = () -> { syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)); }; Thread thread1 = new Thread(listOperations); Thread thread2 = new Thread(listOperations); thread1.start(); thread2.start(); thread1.join(); thread2.join(); assertThat(syncCollection.size()).isEqualTo(12); } 

Come mostrato sopra, creare una vista sincronizzata della raccolta fornita con questo metodo è molto semplice.

Per dimostrare che il metodo restituisce effettivamente una raccolta thread-safe, creiamo prima un paio di thread.

Successivamente, iniettiamo un'istanza Runnable nei loro costruttori, sotto forma di espressione lambda. Teniamo presente che Runnable è un'interfaccia funzionale, quindi possiamo sostituirla con un'espressione lambda.

Infine, controlliamo solo che ogni thread aggiunga effettivamente sei elementi alla raccolta sincronizzata, quindi la sua dimensione finale è dodici.

3. Il metodo synchronizedList ()

Allo stesso modo, in modo simile al metodo synchronizedCollection () , possiamo utilizzare il wrapper synchronizedList () per creare un elenco sincronizzato .

Come ci si potrebbe aspettare, il metodo restituisce una visualizzazione thread-safe dell'elenco specificato :

List syncList = Collections.synchronizedList(new ArrayList());

Non sorprende che l'uso del metodo synchronizedList () sia quasi identico alla sua controparte di livello superiore, synchronizedCollection () .

Pertanto, come abbiamo appena fatto nel precedente unit test, una volta creato un elenco sincronizzato , possiamo generare diversi thread. Dopo averlo fatto, li useremo per accedere / manipolare l' elenco di destinazione in modo thread-safe.

Inoltre, se vogliamo iterare su una raccolta sincronizzata e prevenire risultati imprevisti, dobbiamo fornire esplicitamente la nostra implementazione thread-safe del ciclo. Quindi, potremmo ottenere ciò utilizzando un blocco sincronizzato :

List syncCollection = Collections.synchronizedList(Arrays.asList("a", "b", "c")); List uppercasedCollection = new ArrayList(); Runnable listOperations = () -> { synchronized (syncCollection) { syncCollection.forEach((e) -> { uppercasedCollection.add(e.toUpperCase()); }); } }; 

In tutti i casi in cui abbiamo bisogno di iterare su una raccolta sincronizzata, dovremmo implementare questo idioma. Questo perché l'iterazione su una raccolta sincronizzata viene eseguita tramite più chiamate nella raccolta. Pertanto devono essere eseguiti come una singola operazione atomica.

L'utilizzo del blocco sincronizzato garantisce l'atomicità dell'operazione .

4. Il metodo synchronizedMap ()

La classe Collections implementa un altro wrapper di sincronizzazione pulito, chiamato synchronizedMap (). Potremmo usarlo per creare facilmente una mappa sincronizzata .

Il metodo restituisce una vista thread-safe della dotazione Map realizzazione :

Map syncMap = Collections.synchronizedMap(new HashMap()); 

5. Il metodo synchronizedSortedMap ()

C'è anche un'implementazione controparte del metodo synchronizedMap () . Si chiama synchronizedSortedMap () , che possiamo usare per creare un'istanza SortedMap sincronizzata :

Map syncSortedMap = Collections.synchronizedSortedMap(new TreeMap()); 

6. Il metodo synchronizedSet ()

Successivamente, andando avanti in questa recensione, abbiamo il metodo synchronizedSet () . Come suggerisce il nome, ci permette di creare Set sincronizzati con il minimo sforzo.

Il wrapper restituisce una raccolta thread-safe supportata dal Set specificato :

Set syncSet = Collections.synchronizedSet(new HashSet()); 

7. Il metodo synchronizedSortedSet ()

Infine, l'ultimo wrapper di sincronizzazione che mostreremo qui è synchronizedSortedSet () .

Simile ad altre implementazioni wrapper che abbiamo esaminato finora, il metodo restituisce una versione thread-safe del SortedSet dato :

SortedSet syncSortedSet = Collections.synchronizedSortedSet(new TreeSet()); 

8. Raccolte sincronizzate e simultanee

Fino a questo punto, abbiamo esaminato più da vicino i wrapper di sincronizzazione del framework delle raccolte.

Now, let's focus on the differences between synchronized collections and concurrent collections, such as ConcurrentHashMap and BlockingQueue implementations.

8.1. Synchronized Collections

Synchronized collections achieve thread-safety through intrinsic locking, and the entire collections are locked. Intrinsic locking is implemented via synchronized blocks within the wrapped collection's methods.

As we might expect, synchronized collections assure data consistency/integrity in multi-threaded environments. However, they might come with a penalty in performance, as only one single thread can access the collection at a time (a.k.a. synchronized access).

For a detailed guide on how to use synchronized methods and blocks, please check our article on the topic.

8.2. Concurrent Collections

Concurrent collections (e.g. ConcurrentHashMap), achieve thread-safety by dividing their data into segments. In a ConcurrentHashMap, for example, different threads can acquire locks on each segment, so multiple threads can access the Map at the same time (a.k.a. concurrent access).

Concurrent collections are much more performant than synchronized collections, due to the inherent advantages of concurrent thread access.

So, the choice of what type of thread-safe collection to use depends on the requirements of each use case, and it should be evaluated accordingly.

9. Conclusione

In questo articolo, abbiamo esaminato in modo approfondito il set di wrapper di sincronizzazione implementato nella classe Collections .

Inoltre, abbiamo evidenziato le differenze tra le raccolte sincronizzate e simultanee e abbiamo anche esaminato gli approcci che implementano per ottenere la sicurezza dei thread.

Come al solito, tutti gli esempi di codice mostrati in questo articolo sono disponibili su GitHub.