Aller au contenu principal

Atomics

  • Parfois, tu n'as pas besoin d'une liste ou d'une map.
  • Tu veux juste protéger un simple int, un long, ou un objet contre l'accès concurrent.
  • Synchroniser avec un synchronized est trop lourd pour juste changer un chiffre !

Java fournit donc les classes atomiques dans java.util.concurrent.atomic.

Exemples de classes Atomiques

ClasseSert à
AtomicIntegerUn int qui peut être modifié de manière thread-safe
AtomicLongUn long thread-safe
AtomicBooleanUn boolean thread-safe
AtomicReference<T>Une référence vers n'importe quel objet, thread-safe

Comment ça marche ?

  • Elles utilisent des instructions processeur de type Compare-And-Swap (CAS), sans verrou (lock-free).
  • Super rapides pour des opérations simples.

Exemple avec AtomicInteger :

AtomicInteger counter = new AtomicInteger(0);

// Incrémentation thread-safe
counter.incrementAndGet();

// Lire la valeur actuelle
int value = counter.get();

Aucun besoin de synchronized, de lock, etc. C'est automatique et super rapide.

Exemple complet : compte bancaire

Reprenons l'exemple du cours 7.2 avec deux guichets qui retirent du même compte.

Le problème (sans protection)

class CompteBancaire {
private int solde = 1000;

public void retirer(int montant) {
if (solde >= montant) {
solde -= montant; // pas atomique — race condition possible
}
}

public int getSolde() { return solde; }
}

CompteBancaire compte = new CompteBancaire();
Thread guichet1 = new Thread(() -> compte.retirer(700));
Thread guichet2 = new Thread(() -> compte.retirer(700));
guichet1.start();
guichet2.start();
guichet1.join();
guichet2.join();
System.out.println("Solde : " + compte.getSolde()); // peut afficher -400 !

Solution avec synchronized (cours 7.2)

class CompteBancaire {
private int solde = 1000;

public synchronized void retirer(int montant) {
if (solde >= montant) {
solde -= montant;
}
}

public synchronized int getSolde() { return solde; }
}

Ça fonctionne, mais synchronized verrouille tout l'objet — même getSolde() doit attendre si un thread est dans retirer().

Solution avec AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

class CompteBancaire {
private AtomicInteger solde = new AtomicInteger(1000);

public void retirer(int montant) {
solde.updateAndGet(s -> s >= montant ? s - montant : s);
}

public int getSolde() { return solde.get(); }
}

CompteBancaire compte = new CompteBancaire();
Thread guichet1 = new Thread(() -> compte.retirer(700));
Thread guichet2 = new Thread(() -> compte.retirer(700));
guichet1.start();
guichet2.start();
guichet1.join();
guichet2.join();
System.out.println("Solde : " + compte.getSolde()); // garanti : 300 ou 1000, jamais -400

updateAndGet applique la fonction de manière atomique via Compare-And-Swap : si le solde a changé entre la lecture et l'écriture, il réessaie — sans jamais bloquer les autres threads.

Attention : AtomicInteger est idéal quand la logique tient en une seule variable. Si tu dois vérifier ou modifier plusieurs champs en même temps (ex. : solde + historique), synchronized reste nécessaire.

Comparaison

synchronizedAtomicInteger
MécanismeVerrou (bloque les autres threads)CAS sans verrou
PerformancePlus lent sous forte contentionPlus rapide pour une seule variable
LisibilitéIntuitiveRequiert de connaître updateAndGet
Cas d'usageLogique complexe, plusieurs champsUn seul champ numérique simple

Méthodes disponibles

AtomicInteger / AtomicLong

MéthodeDescription
get()Lit la valeur actuelle
set(val)Écrit une nouvelle valeur
getAndSet(val)Lit, puis remplace par val
incrementAndGet()++i thread-safe — retourne la nouvelle valeur
getAndIncrement()i++ thread-safe — retourne l'ancienne valeur
decrementAndGet()--i thread-safe
getAndDecrement()i-- thread-safe
addAndGet(delta)Ajoute delta, retourne la nouvelle valeur
getAndAdd(delta)Ajoute delta, retourne l'ancienne valeur
updateAndGet(fn)Applique une fonction atomiquement, retourne la nouvelle valeur
getAndUpdate(fn)Applique une fonction atomiquement, retourne l'ancienne valeur
compareAndSet(expected, update)Remplace par update seulement si la valeur actuelle est expected

AtomicBoolean

MéthodeDescription
get()Lit la valeur actuelle
set(val)Écrit une nouvelle valeur
getAndSet(val)Lit, puis remplace par val
compareAndSet(expected, update)Remplace par update seulement si la valeur actuelle est expected

AtomicReference<T>

MéthodeDescription
get()Retourne l'objet référencé
set(val)Remplace la référence
getAndSet(val)Lit la référence actuelle, puis la remplace
updateAndGet(fn)Applique une fonction sur l'objet, retourne le nouvel objet
getAndUpdate(fn)Applique une fonction sur l'objet, retourne l'ancien objet
compareAndSet(expected, update)Remplace par update seulement si la référence actuelle est expected

compareAndSet est le cœur du CAS : « ne modifie que si personne d'autre n'a changé la valeur entre ma lecture et mon écriture ».

Exemple avec AtomicReference<T>

AtomicReference permet de remplacer une référence vers un objet de façon thread-safe — utile quand ce qu'on veut protéger n'est pas un nombre, mais un objet.

Cas : garder en mémoire la dernière transaction effectuée

Plusieurs guichets accèdent au même compte. On veut stocker la dernière transaction (montant + guichet) sans race condition.

import java.util.concurrent.atomic.AtomicReference;

class Transaction {
String guichet;
int montant;

Transaction(String guichet, int montant) {
this.guichet = guichet;
this.montant = montant;
}

public String toString() {
return guichet + " a retiré " + montant + "$";
}
}

class CompteBancaire {
private AtomicInteger solde = new AtomicInteger(1000);
private AtomicReference<Transaction> derniereTx = new AtomicReference<>(null);

public void retirer(String guichet, int montant) {
int ancienSolde = solde.get();
int nouveauSolde = solde.updateAndGet(s -> s >= montant ? s - montant : s);
if (nouveauSolde < ancienSolde) { // le solde a diminué → retrait effectué
derniereTx.set(new Transaction(guichet, montant));
}
}

public int getSolde() { return solde.get(); }
public Transaction getDerniereTx() { return derniereTx.get(); }
}

// Dans le main :
CompteBancaire compte = new CompteBancaire();

Thread guichet1 = new Thread(() -> compte.retirer("Guichet 1", 200));
Thread guichet2 = new Thread(() -> compte.retirer("Guichet 2", 300));

guichet1.start();
guichet2.start();
guichet1.join();
guichet2.join();

System.out.println("Solde : " + compte.getSolde());
System.out.println("Dernière transaction : " + compte.getDerniereTx());
// ex : Guichet 2 a retiré 300$

AtomicReference.set() remplace la référence atomiquement — peu importe combien de threads appellent set() en même temps, on ne peut jamais se retrouver avec une référence à moitié écrite.

AtomicInteger protège un nombre. AtomicReference protège une référence vers un objet — n'importe quel type.

Résumé des Atomic

  • Idéal pour des champs individuels modifiés par plusieurs threads.
  • Plus rapide que de synchroniser un bloc entier.
  • Ne remplace pas une collection complète : si tu veux gérer plusieurs éléments, il faut une ConcurrentHashMap, CopyOnWriteArrayList, etc.