List
, de
Set
et de
Map
, à la fois
thread-safe
et performantes.
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.
Exemple 72. Concurrence sur une table de hachage
public class ConcurrentAccessMap { // création d'une map classique private Map<String, Marin> map = new HashMap<String, Marin>() ; public boolean addIfAbsent(Marin m) { // on ajoute notre marin que s'il ne s'y trouve pas déjà if (!map.keySet().contains(m.getNom())) { map.put(m.getNom(), m) ; return true ; } return false ; } }
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.
Exemple 73. Concurrence sur une collection
public class ConcurrentAccessCollection { // création d'une liste classique private List<Marin> liste = new ArrayList<Marin>() ; 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) { return liste.remove(marin) ; } }
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.
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.
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.
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.
ConcurrentMap<K, V>
est une extension de
Map<K, V>
, 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
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
.
ConcurrentHashMap
.
Son implémentation est la classe
ConcurrentHashMap<K, V>
. 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.
ConcurrentSkipListSet<K, V>
et
ConcurrentSkipListMap<K, V>
. 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.