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
synchronizedest trop lourd pour juste changer un chiffre !
Java fournit donc les classes atomiques dans java.util.concurrent.atomic.
Exemples de classes Atomiques
| Classe | Sert à |
|---|---|
AtomicInteger | Un int qui peut être modifié de manière thread-safe |
AtomicLong | Un long thread-safe |
AtomicBoolean | Un 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 :
AtomicIntegerest 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),synchronizedreste nécessaire.
Comparaison
synchronized | AtomicInteger | |
|---|---|---|
| Mécanisme | Verrou (bloque les autres threads) | CAS sans verrou |
| Performance | Plus lent sous forte contention | Plus rapide pour une seule variable |
| Lisibilité | Intuitive | Requiert de connaître updateAndGet |
| Cas d'usage | Logique complexe, plusieurs champs | Un seul champ numérique simple |
Méthodes disponibles
AtomicInteger / AtomicLong
| Méthode | Description |
|---|---|
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éthode | Description |
|---|---|
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éthode | Description |
|---|---|
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 |
compareAndSetest 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.
AtomicIntegerprotège un nombre.AtomicReferenceprotè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.