Les problèmes de concurrence d'accès ne se pose pas que pour les champs de type primitif ou objet. Ils se posent de façon également aiguë lorsque l'on manipule des collections et des tables de hachage. C'est la raison pour laquelle l'API Concurrent propose plusieurs implémentations de List
, de Set
et de Map
, à la fois thread-safe et performantes.
L'API Java nous offre déjà quelques solutions pour construire des collections thread-safe . Les classes 3617400 Bottines Bottines Femme 3617400 Femme 3617400 Hirschkogel Hirschkogel Hirschkogel Vector
et Hashtable
sont synchronisées, et la classe Collections
expose plusieurs méthodes pour créer des collections synchronisées, par enveloppement de ces collections et synchronisation des accès. Malheureusement ces classes ne sont pas suffisantes pour traiter de façon atomique des problèmes simples.
Considérons le simple code suivant.
Cette implémentation ne fonctionne pas en concurrence d'accès, et changer notre implémentation, ici HashMap
en Hashtable
ne changerait rien à l'affaire. Si l'on applique les mêmes principes d'analyse que dans les paragraphes précédents, on se rend compte que deux thread différents peuvent se trouver en même temps dans le bloc du if(...)
, portant deux marins de même nom. Ces deux marins seront donc ajoutés, l'un après l'autre, ce que l'on veut précisément éviter.
On pourrait synchroniser la méthode addIfAbsent()
, et cela résoudrait le problème. L'API Concurrent nous offre une autre solution, comme nous allons le voir.
Prenons un autre exemple, cette fois sur une collection.
Exemple 73. Concurrence sur une collection
public class ConcurrentAccessCollection { // création d'une liste classique private Listliste = new ArrayList () ; public Marin getLastMarin() { // on détermine l'index du dernier élément int lastIndex = liste.length() - 1 ; return liste.get(lastIndex) ; } public boolean remove(Marin marin) { Femme Hirschkogel 3617400 Bottines Hirschkogel Femme Hirschkogel 3617400 Bottines 3617400 return liste.remove(marin) ; } }
De même, on constate après analyse que cette classe n'est pas thread-safe . Si un premier thread appelle getLastMarin()
, et est interrompu entre le moment où il calcule lastIndex
et le moment où il lit effectivement ce dernier marin, alors qu'un autre retire un marin de la liste, par la méthode remove(Marin m)
, alors une erreur sera générée, puisque la valeur de lastIndex
sera trop grande.
Encore une fois, synchroniser le tout résoudrait le problème, mais l'API Concurrent permet de mieux faire.
Synchroniser une collection ou une table, comme c'est le cas pour Vector
et Hashtable
n'apporte pas toujours de solution satisfaisante, voire pas de solution du tout. Une collection utilisée en concurrence d'accès peut poser des problèmes, même si tous ses accès sont synchronisés.
Comme nous l'avons déjà vu, itérer sur une collection alors qu'une autre partie de l'application ajoute des éléments dedans peut mener à une ConcurrentModificationException
. Or, quelques méthodes peuvent itèrer en interne, sans exposer explicitement ce comportement. C'est le cas de hashCode()
, et des méthodes du type addAll()
, containsAll()
, removeAll()
ou retainAll()
. Dans tous ces cas, la synchronisation n'est pas suffisante, on a besoin d'un mécanisme différent, qui nous garantisse le fonctionnement dans tous les cas possibles de la concurrence d'accès.
L'API Concurrent propose de nouvelles implémentations de quelques interfaces de l'API Collection, qui enrichissent ces interfaces de quelques méthodes, et apportent des solutions complètes aux problèmes que nous venons de présenter.
On ne parle pas de collections synchronisées pour ces nouvelles interfaces et classes, mais plutôt de collections concurrentes. Ces collections concurrentes sont utilisées pour résoudre les problèmes de forte concurrence d'accès, sans sacrifier aux pertes de performances que peut entraîner la synchronisation à outrance.
Deux classes sont apportées par l'API Concurrent, implémentation respectivement de List
et de Set
: CopyOnWriteArrayList
et CopyOnWriteArraySet
.
L'implémentation CopyOnWriteArrayList
apporte deux opérations supplémentaires à List
, qui sont atomiques : addItAbsent(T t)
et addAllAbstent(Collection extends E> collection)
.
Ces deux classes peuvent être utilisées en concurrence d'accès. On peut itérer dessus sans risque qu'une exception de type ConcurrentModificationException
soit jetée.
Hirschkogel 3617400 3617400 Hirschkogel 3617400 Femme Hirschkogel Bottines Bottines Femme Ces deux collections sont construites sur des tableaux d'objets, qui sont recopiés intégralement à chaque fois qu'une modification est effectuée. Seul le changement de tableau (qui n'est qu'un jeu de pointeurs) est synchronisé. Toutes les opérations de lecture sont effectuées sur un unique tableau, en lecture seule, qui ne pose donc aucun problème de modification concurrente.
Ces deux classes ne sont donc pas utilisables dans tous les cas. Les lectures sont très performantes, mais les écritures très peu performantes. Les bons cas sont donc ceux dans lesquels les lectures sont beaucoup plus fréquentes que les modifications, et pour les collections qui ne sont pas trop volumineuses.
L'API Concurrent fournit dans ce domaine une interface et quelques implémentations. Cette interface ajoute quatre méthodes, que nous allons voir.
L'interface ConcurrentMap
est une extension de Map
, qui propose les méthodes supplémentaires suivantes.
putIfAbsent(K key, V value)
: ajoute ce couple (clé, valeur) si cette clé n'est pas déjà présente.
remove(Object key, Object value)
: retire cette clé de la table, si elle est associée à la valeur passée en paramètre. Cela permet d'éviter de retirer une clé qu'un autre Hirschkogel 3617400 Femme Bottines Hirschkogel 3617400 3617400 Hirschkogel Bottines Femme thread aurait ajoutée par ailleurs.
replace(K key, V value)
: remplace la valeur associée à cette clé par celle passée en paramètre.
replace(K key, V oldValue, V newValue)
: remplace la valeur associée à cette clé par celle passée en paramètre, si l'actuelle valeur est oldValue
.
L'implémentation fournie par l'API Concurrent est ConcurrentHashMap
.
Son implémentation est la classe ConcurrentHashMap
. Elle autorise autant de lectures que l'on veut, et ces lectures sont non-bloquantes. Les itérateurs construits sur cette table ne jettent pas de ConcurrentModificationException
.
En revanche, les opérations de modifications sont contrôlées. Lors de la construction de cette table, on peut lui passer un paramètre, concurrencyLevel
(la valeur par défaut est 16), qui règle certains éléments internes de la table. Le but est de réduire les goulets d'étranglement, tout en conservant la sécurité des opérations concurrentes.
L'API Concurrent nous donne également deux implémentations de ces interfaces, qui, rappelons-le, permettent de conserver les éléments enregistrés triés.
Les deux implémentations fournies sont ConcurrentSkipListSet
et ConcurrentSkipListMap
. De la même façon que les autres classes, elles sont optimisées pour les accès en lecture, notamment pour être non-bloquantes.
Table des matières