I. Présentation

Depuis Java 1.5, le package java.util.concurrent.atomic fournit une API permettant de gérer des variables de manière concurrente sans utiliser de verrous.

II. Variables atomiques (Atomic*)

Les classes de bases sont AtomicBoolean, AtomicInteger, AtomicLong et AtomicReference. Toutes ces classes sont basées sur la pseudo-interface suivante :

  • get : lit la valeur de la variable avec le même effet que son équivalent volatile.
  • set : écrit la valeur de la variable avec le même effet que son équivalent volatile.
  • lazySet (1.6+) : essaie de modifier la valeur sans garantie sur l'ordre des écritures. Cela permet de combiner une écriture non-volatile avec d'autres qui le sont.
  • weakCompareAndSet : effectue une lecture atomique, puis tente une écriture conditionnelle (si la valeur était bien celle attendue). Tout comme précédemment, il n'y a pas de garantie sur l'écriture. La valeur de retour permet de savoir si l'écriture a réussi.
  • compareAndSet : effectue une lecture-écriture conditionnelle et atomique.
  • getAndSet : effectue une lecture-écriture atomique et renvoie la valeur précédente.

AtomicInteger et AtomicLong étant des nombres, elles offrent également des méthodes dédiées :

  • addAndGet / decrementAndGet / incrementAndGet : incrémente (de manière atomique) la valeur et renvoie la valeur après modification.
  • getAndAdd / getAndDecrement / getAndIncrement : idem que précédemment mais renvoie la valeur avant modification.

Avec l'arrivée de Java 1.8 (et de la programmation fonctionnelle), de nouvelles méthodes ont été ajoutées pour offrir plus de souplesses :

  • accumulateAndGet : effectue une lecture-écriture atomique avec le résultat de la fonction binaire (à deux arguments) passée en paramètre, puis renvoie la valeur après modification. La fonction est appelée (en boucle jusqu'à la réussite de l'opération) avec en premier paramètre la valeur courante et la valeur de mise à jour (premier paramètre).
  • getAndAccumulate : idem que précédemment mais renvoie la valeur avant modification.
  • updateAndGet : effectue une lecture-écriture atomique avec le résultat de la fonction unaire (à un seul argument) passée en paramètre, puis renvoie la valeur après modification. La fonction est appelée (en boucle jusqu'à la réussite de l'opération) avec en paramètre la valeur courante.
  • getAndUpdate : idem que précédemment mais renvoie la valeur avant modification.

Il n'existe pas de type atomique dédié aux flottants. Cependant, étant de même taille que les entiers, on peut utiliser un AtomicInteger combiné aux conversions Float.floatToRawIntBits(float) et Float.intBitsToFloat(int). La même approche pour être utilisée pour gérer des doubles avec AtomicLong, Double.doubleToRawLongBits(double) et Double.longBitsToDouble(long).

III. Tableaux atomiques (Atomic*Array)

Le mot-clé volatile s'applique à un champ d'une classe mais ne s'applique pas aux éléments d'un tableau. L'API concurrente de Java offre donc les types AtomicIntegerArray, AtomicLongArray et AtomicReferenceArray. Comme pour les tableaux, ils sont de tailles fixes ; et comme pour les variables atomiques, ils offrent les mêmes pseudo-interface. Ils peuvent s'initialiser avec une taille avec ou un tableau existant. En revanche contrairement au tableau classique : ils ne peuvent être itérés et ne peuvent être copiés via la méthode System.arraycopy.

Il n'existe pas de type dédié aux booléens mais vous pouvez utilisez n'importe quel entier pour symboliser true et false.

Pourquoi n'existe-il pas d'AtomicBitSet ?

Tout simplement parce qu'il n'existe pas d'instruction bas-niveau permettant la lecture-écriture atomique bit-à-bit. Il est possible de simuler en utilisant un AtomicIntegerArray pour stocker des groupes de bits. Néanmoins, chaque bit au sein d'un groupe est dépendant des autres. Par exemple, si nous avons deux groupes de 2 bits, la lecture-écriture des deux premiers bits reposent sur la même référence et vous perdrez tous les avantages de l'API atomic. En revanche, les accès au troisième et au quatrième bit s'effectuant sur des groupes différents, on peut utiliser pleinement les avantages des variables atomiques.

IV. Accesseurs atomiques (Atomic*FieldUpdater)

Si vous avez des champs volatiles et souhaitez bénéficier des traitements atomiques, l'API vous propose les classes AtomicIntegerFieldUpdater, AtomicLongFieldUpdater et AtomicReferenceFieldUpdater. Pour créer une instance, il faut utiliser la méthode statique newUpdater avec en paramètre la classe qui contient le champ, le nom du champ volatile et, pour les références, le type du champ. On retrouve exactement la même pseudo-interface, excepté qu'il faut passer en argument une instance qui contient le champ en question.

Néanmoins, la méthode compareAndSet des Atomic*FieldUpdater offre moins de garantie que sa version originale, puisque l'atomicité est uniquement garantie pour les modifications effectuées par le biais du même « updater ». Mais alors pourquoi ne pas utiliser directement des variables atomiques ? Réponse : la mémoire. Les variables atomiques sont des objets et qui donc occupent de la mémoire. Imaginez que vous ayez une liste chaînée dont chaque nœud est implémenté par une AtomicReference. Si vous créez de grandes quantités de nœuds et/ou de liste, vous allez créer autant de références, même si chaque nœud ne pointe sur rien de concret. Voici quelques petits programmes de démonstration :

La mémoire mesurée ne reflète pas l'impact exact puisqu'on lit la mémoire réservée. Or celle-ci n'augmente pas linéairement aux besoins mais par paliers. Cependant, les ordres de grandeur sont suffisamment grands pour se faire une idée de l'impact.

  • Ce premier programme sert de point de repère en créant des instances d'une classe vide :

    AtomicFieldUpdater - Utilisation mémoire à vide (code)
    Sélectionnez
    //com.developpez.lmauzaize.java.concurrence.atomic.AtomicFieldUpdaterUtilisationMemoireVide
    class Reference {}
    
    int taille = 32_000_000;
    Object[] objets = new Object[taille];
    long avant = Runtime.getRuntime().totalMemory();
    
    for (int i = 0; i < taille; i++) {
      objets[i] = new Reference();
    }
    
    long après = Runtime.getRuntime().totalMemory();
    Logger.println("Avant: %,10dk", avant/1024);
    Logger.println("Après: %,10dk (+ %,10dk)", après/1024, (après - avant)/1024);
    AtomicFieldUpdater - Utilisation mémoire à vide (console)
    Sélectionnez
    00:00:00.016 [main           ] Avant:    133 120k
    00:00:03.573 [main           ] Après:    711 168k (+    578 048k)
    On crée trente-deux millions d'instances, ce qui a fait augmenté la mémoire réservée de 564Mo.
  • Même chose que précédemment avec une variable atomique :

    AtomicFieldUpdater - Utilisation mémoire avec une variable atomique (code)
    Sélectionnez
    //com.developpez.lmauzaize.java.concurrence.atomic.AtomicFieldUpdaterUtilisationMemoireVariableAtomique
    class Reference {
      AtomicReference<Object> ref = new AtomicReference<>();
    }
    
    int taille = 32_000_000;
    Reference[] objets = new Reference[taille];
    long avant = Runtime.getRuntime().totalMemory();
    
    for (int i = 0; i < taille; i++) {
      objets[i] = new Reference();
    }
    
    long après = Runtime.getRuntime().totalMemory();
    Logger.println("Avant: %,10dk", avant/1024);
    Logger.println("Après: %,10dk (+ %,10dk)", après/1024, (après - avant)/1024);
    AtomicFieldUpdater - Utilisation mémoire avec une variable atomique (console)
    Sélectionnez
    00:00:00.018 [main           ] Avant:    133 120k
    00:00:00.028 [main           ] Après:  1 261 568k (+  1 128 448k)
    Cette fois-ci, il aura fallu alloué 537Mo de plus pour stocker tous ces objets. Soit environ 18 octets par instance.
  • Maintenant, utilisons un Atomic*FieldUpdater :

    AtomicFieldUpdater - Utilisation mémoire avec un accesseur atomique (code)
    Sélectionnez
    //com.developpez.lmauzaize.java.concurrence.atomic.AtomicFieldUpdaterUtilisationMemoireAccesseurAtomique
    class Reference {
      volatile Object ref;
    }
    
    int taille = 32_000_000;
    Reference[] objets = new Reference[taille];
    long avant = Runtime.getRuntime().totalMemory();
    
    AtomicReferenceFieldUpdater<Reference, Object> accesseur = AtomicReferenceFieldUpdater.newUpdater(Reference.class, Object.class, "ref");
    for (int i = 0; i < taille; i++) {
      objets[i] = new Reference();
    }
    
    long après = Runtime.getRuntime().totalMemory();
    Logger.println("Avant: %,10dk", avant/1024);
    Logger.println("Après: %,10dk (+ %,10dk)", après/1024, (après - avant)/1024);
    AtomicFieldUpdater - Utilisation mémoire avec un accesseur atomique (console)
    Sélectionnez
    00:00:00.016 [main           ] Avant:    133 120k
    00:00:00.024 [main           ] Après:    780 800k (+    647 680k)
    Cette fois-ci, seulement 68Mo supplémentaire ont été nécessaire. Soit environ 2 octets par instance.

    Dans un vrai contexte d'utilisation, le Atomic*FieldUpdater doit être un champ statique et final de la classe qui contient le champ.

V. Accumulateurs (*Adder/*Accumulator) (1.8+)

Les variables atomiques bien que n'utilisant pas de verrous, peuvent souffrir d'une certaine lenteur pour de « simples » compteurs. C'est pourquoi en Java 8 ont été introduites les classes LongAdder/LongAccumulator et DoubleAdder/DoubleAccumulator. Un accumulateur repose sur une opération binaire et un élément de départ. Chaque accumulation consiste à appeler l'opération avec la valeur courante et la valeur d'appel.

Les classes *Adder sont en faites des « alias » des *Accumulator qui utilisent l'addition comme opération et 0 comme élément neutre.

Pour limiter les contentions en écriture, les threads vont utiliser différentes variables. La lecture de la valeur réelle de l'accumulateur nécessite alors de rassembler les différentes variables. Cette classe est donc parfaitement adaptée au cas où les écritures sont hautement concurrentes et beaucoup plus nombreuses que les lectures.

VI. Pair atomique

Pour finir ce tour d'horizon du package atomic, nous allons évoquer rapidement les classes AtomicMarkableReference et AtomicStampedReference. Celles-ci permettent de gérer une association respectivement un booléen et un long avec une référence.

La principale nouveauté tiens dans les méthodes attempt* qui permettent de modifier atomiquement le booléen/long associé si la référence correspond.

VII. Conclusion

Au cours de ce cinquième article, nous avons vu comment gérer la concurrence de données "simple" sans utiliser de verrous. Ceci permet de réaliser des mises à jours unitaires et cohérentes tout en pouvant être concurrentes. Le prochain chapitre sera dédié à donner quelques éléments pour bien appréhender les problématiques de développement concurrent avec tous les outils déjà présentés.

VIII. Remerciements

Je remercie tous les contributeurs de l'API Java et les packages java.util.concurrent.* et plus particulièrement Doug Lea qui est l'un des principaux auteurs.

Je remercie également Mickael Baron, Thierry Leriche-Dessirier alias thierryler, Claude Leloup et f-leb pour leur relecture attentive, leurs remarques et leurs bons conseils.

Je tiens aussi à remercier la communauté Developpez.com qui a mis en place tous les outils, procédures et l'hébergement nécessaires à la publication de cet article.

Enfin mon épouse et mes enfants pour leur patience et leur tolérance durant les nombreuses heures qui ont été nécessaires à la rédaction de cet article.

IX. Annexes

X-A. Sources des exemples

Tous les exemples donnés dans cet article sont disponibles sous la forme d'un projet Maven hébergé sous GitHub. Tous les exemples cités contiennent une première ligne commentaire indiquant l'emplacement du fichier dans les sources. Les sources propres à ce chapitre se trouvent sous le package com.developpez.lmauzaize.java.concurrence.ch04_verrous.

Si vous ne savez pas comment importer le projet, je vous invite à consulter l'article « Importer un projet Maven dans Eclipse en 5 minutes ».

X-B. Java Concurrent Animated

Java Concurrent Animated est un projet Swing visant à montrer graphiquement le comportement de différents composants de l'API concurrente de Java.