Une introduction aux collections Java synchronisées

1. Vue d'ensemble

Le framework de collections est un composant clé de Java. Il fournit un grand nombre d'interfaces et d'implémentations, ce qui nous permet de créer et de manipuler différents types de collections de manière simple.

Bien que l'utilisation de collections non synchronisées simples soit globalement simple, cela peut également devenir un processus intimidant et sujet aux erreurs lorsque vous travaillez dans des environnements multithreads (c'est-à-dire la programmation simultanée).

Par conséquent, la plate-forme Java fournit une prise en charge solide pour ce scénario via différents wrappers de synchronisation implémentés dans la classe Collections .

Ces wrappers facilitent la création de vues synchronisées des collections fournies au moyen de plusieurs méthodes de fabrique statiques.

Dans ce didacticiel, nous allons plonger en profondeur dans ces wrappers de synchronisation statique. Nous soulignerons également la différence entre les collections synchronisées et les collections simultanées .

2. La méthode synchronizedCollection ()

Le premier wrapper de synchronisation que nous aborderons dans ce tour d'horizon est la méthode synchronizedCollection () . Comme son nom l'indique, il renvoie une collection thread-safe sauvegardée par la collection spécifiée .

Maintenant, pour comprendre plus clairement comment utiliser cette méthode, créons un test unitaire de 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); } 

Comme indiqué ci-dessus, la création d'une vue synchronisée de la collection fournie avec cette méthode est très simple.

Pour démontrer que la méthode retourne réellement une collection thread-safe, nous créons d'abord quelques threads.

Après cela, nous injectons ensuite une instance Runnable dans leurs constructeurs, sous la forme d'une expression lambda. Gardons à l'esprit que Runnable est une interface fonctionnelle, nous pouvons donc la remplacer par une expression lambda.

Enfin, nous vérifions simplement que chaque thread ajoute effectivement six éléments à la collection synchronisée, donc sa taille finale est de douze.

3. La méthode synchronizedList ()

De même, comme pour la méthode synchronizedCollection () , nous pouvons utiliser le wrapper synchronizedList () pour créer une liste synchronisée .

Comme on pouvait s'y attendre, la méthode renvoie une vue thread-safe de la liste spécifiée :

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

Sans surprise, l'utilisation de la méthode synchronizedList () semble presque identique à son homologue de niveau supérieur, synchronizedCollection () .

Par conséquent, comme nous venons de le faire dans le test unitaire précédent, une fois que nous avons créé une liste synchronisée , nous pouvons générer plusieurs threads. Après cela, nous les utiliserons pour accéder / manipuler la liste cible de manière thread-safe.

De plus, si nous voulons parcourir une collection synchronisée et éviter des résultats inattendus, nous devons explicitement fournir notre propre implémentation thread-safe de la boucle. Par conséquent, nous pourrions y parvenir en utilisant un bloc synchronisé :

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

Dans tous les cas où nous avons besoin d'itérer sur une collection synchronisée, nous devons implémenter cet idiome. En effet, l'itération sur une collection synchronisée est effectuée via plusieurs appels dans la collection. Par conséquent, ils doivent être effectués en une seule opération atomique.

L'utilisation du bloc synchronisé assure l'atomicité de l'opération .

4. La méthode synchronizedMap ()

La classe Collections implémente un autre wrapper de synchronisation soigné, appelé synchronizedMap (). Nous pourrions l'utiliser pour créer facilement une carte synchronisée .

La méthode retourne une vue thread-safe de l' implémentation Map fournie :

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

5. La méthode synchronizedSortedMap ()

Il existe également une implémentation homologue de la méthode synchronizedMap () . Il s'appelle synchronizedSortedMap () , que nous pouvons utiliser pour créer une instance SortedMap synchronisée :

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

6. La méthode synchronizedSet ()

Ensuite, dans cette revue, nous avons la méthode synchronizedSet () . Comme son nom l'indique, il nous permet de créer des ensembles synchronisés avec un minimum de tracas.

L'encapsuleur renvoie une collection thread-safe sauvegardée par le Set spécifié :

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

7. La méthode synchronizedSortedSet ()

Enfin, le dernier wrapper de synchronisation que nous allons présenter ici est synchronizedSortedSet () .

Semblable à d'autres implémentations de wrapper que nous avons examinées jusqu'à présent, la méthode retourne une version thread-safe du SortedSet donné :

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

8. Collections synchronisées et simultanées

Jusqu'à présent, nous avons examiné de plus près les wrappers de synchronisation du framework de collections.

Maintenant, concentrons-nous sur les différences entre les collections synchronisées et les collections simultanées , telles que les implémentations ConcurrentHashMap et BlockingQueue .

8.1. Collections synchronisées

Les collections synchronisées assurent la sécurité des threads grâce au verrouillage intrinsèque, et toutes les collections sont verrouillées . Le verrouillage intrinsèque est implémenté via des blocs synchronisés dans les méthodes de la collection encapsulée.

Comme on pouvait s'y attendre, les collections synchronisées assurent la cohérence / l'intégrité des données dans les environnements multi-threads. Cependant, ils peuvent entraîner une pénalité de performances, car un seul thread peut accéder à la collection à la fois (c'est-à-dire un accès synchronisé).

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. Conclusion

Dans cet article, nous avons examiné en profondeur l'ensemble des wrappers de synchronisation implémentés dans la classe Collections .

De plus, nous avons mis en évidence les différences entre les collections synchronisées et simultanées, et avons également examiné les approches qu'ils implémentent pour assurer la sécurité des threads.

Comme d'habitude, tous les exemples de code présentés dans cet article sont disponibles à l'adresse over sur GitHub.