Modèle mémoire Java
Le modèle mémoire Java — quelles lectures et écritures sont visibles entre threads et comment fonctionne happens-before.
Le modèle mémoire Java (JMM) est la partie de la spécification du langage qui définit quand un thread est garanti de voir les écritures d'un autre thread. C'est le règlement derrière volatile, synchronized, et final — et la raison pour laquelle le code multithread correct a la forme qu'il a.
Ce chapitre explique pourquoi le modèle existe, comment la relation happens-before rassemble tout, et quel outil utiliser face à un problème de visibilité, d'atomicité ou de réordonnancement.
Pourquoi le modèle mémoire existe
Sur le matériel moderne, la valeur qu'un thread « écrit » dans un champ peut rester dans un registre CPU ou un cache local au cœur bien avant d'atteindre la mémoire principale, et le compilateur est libre de réordonner les instructions indépendantes. Sans règles, un thread pourrait modifier un champ tandis qu'un autre thread ne verrait jamais le changement — ou le verrait dans le mauvais ordre.
Le JMM définit une garantie unique qui maîtrise tout cela : la relation happens-before. Si l'action A se produit avant l'action B (happens-before), alors les effets de A sont visibles pour B. Tout le reste — volatile, les verrous, final, les démarrages et jointures de threads — n'est qu'un moyen de créer un arc happens-before.
// Without synchronization, this loop may NEVER terminate:
// the reader thread can cache 'running' forever and miss the write.
static boolean running = true; // plain field — no guarantee
void reader() { while (running) { /* spin */ } } // may hang
void stopper() { running = false; } // may go unseenLe mot-clé volatile
Déclarer un champ volatile fait deux choses : chaque lecture va en mémoire principale (visibilité), et une écriture volatile se produit avant (happens-before) toute lecture volatile ultérieure du même champ (ordonnancement). Cela ne rend pas atomiques les opérations composées comme count++.
public class Worker {
private volatile boolean running = true; // visible across threads
public void run() {
while (running) { // always sees the latest value
doWork();
}
}
public void stop() {
running = false; // guaranteed visible to run()
}
}Utilisez volatile pour un seul drapeau ou une référence lue par de nombreux threads et écrite par un seul. Recourez-y lorsque vous avez besoin de visibilité, pas d'exclusion mutuelle. Voir Java volatile pour un traitement plus approfondi.
Happens-Before : les règles fondamentales
Happens-before est le contrat contre lequel vous programmez réellement. Ces arcs sont ceux que vous créez intentionnellement :
| Règle | Arc happens-before |
|---|---|
| Ordre du programme | Chaque action dans un thread se produit avant les actions ultérieures dans ce même thread |
| Verrou moniteur | Le déverrouillage d'un moniteur se produit avant le verrouillage ultérieur du même moniteur |
| Volatile | Une écriture dans un champ volatile se produit avant toute lecture ultérieure de ce champ |
| Démarrage de thread | thread.start() se produit avant toute action dans le thread démarré |
| Jointure de thread | Toutes les actions d'un thread se produisent avant qu'un autre thread retourne de son join() |
| Champs final | Les écritures du constructeur dans les champs final se produisent avant la publication de l'objet |
// synchronized creates a happens-before edge through the same lock:
synchronized (lock) { shared = compute(); } // unlock here ...
// ... happens-before another thread's:
synchronized (lock) { use(shared); } // ... lock hereAtomicité vs. visibilité
Ce sont deux problèmes différents qui nécessitent des outils différents. volatile corrige la visibilité mais pas l'atomicité ; synchronized et les classes java.util.concurrent.atomic corrigent les deux pour la section qu'elles couvrent.
| Problème | Symptôme | Correction |
|---|---|---|
| Visibilité | Un thread ne voit jamais une valeur mise à jour | volatile, synchronized, final |
| Atomicité | Mises à jour perdues avec x++ sous contention | synchronized, AtomicInteger, verrous |
| Réordonnancement | Les opérations apparaissent dans le mauvais ordre | happens-before via les outils ci-dessus |
import java.util.concurrent.atomic.AtomicLong;
public class Counter {
private final AtomicLong hits = new AtomicLong();
public void record() { hits.incrementAndGet(); } // atomic + visible
public long total() { return hits.get(); }
}Champs final et publication sûre
Un champ final défini dans le constructeur est figé au retour du constructeur. Tout thread qui voit un objet correctement construit (dont la référence n'a pas fui depuis le constructeur) a la garantie de voir les valeurs correctes de ses champs final — sans volatile ni verrou. C'est pourquoi les objets immuables sont intrinsèquement thread-safe.
public final class Point {
private final int x, y; // frozen at construction
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
}
// Share a Point across threads freely: its final fields are safely published.Un exemple autonome
L'exemple exécutable ci-dessous n'utilise que le JDK. Il met en œuvre quatre outils du modèle mémoire dans un seul programme : volatile pour la visibilité entre threads, AtomicInteger pour un comptage sans perte de mise à jour, les champs final pour la publication sûre, et synchronized pour l'accumulation atomique.
Ce qu'il faut retenir de l'exécution :
reader saw data = 42prouve que l'écriturevolatiledansflaga publié l'écriture ordinaire dansdata— le lecteur est garanti de la voir grâce à l'arc happens-before.atomic counter = 800000 (expected 800000)montre queAtomicInteger.incrementAndGet()n'a perdu aucune mise à jour parmi 8 threads effectuant chacun 100 000 incréments — unint++ordinaire afficherait un nombre plus petit et non déterministe.final config = prod:443démontre la publication sûre : les champsfinaldeConfigsont corrects sans aucunvolatileni verrou.synchronized sum = 10000confirme que les quatre threads écrivains (1000+2000+3000+4000) ont accumulé via le même moniteur sans perte d'addition.- Chaque ligne de sortie correspond à un mécanisme happens-before différent, et pourtant ils se combinent dans un seul programme — les outils du JMM sont complémentaires, pas interchangeables.
Choisir le bon outil
Un guide de décision rapide lorsque vous cherchez un mécanisme du modèle mémoire :
- Un seul écrivain, de nombreux lecteurs d'un drapeau ou d'une référence ? Utilisez
volatile. - Compteurs, accumulateurs ou compare-and-set sur une seule variable ? Utilisez les classes atomiques — elles évitent la surcharge des verrous.
- Une mise à jour en plusieurs étapes qui doit être tout-ou-rien ? Protégez-la avec
synchronized(ou un verrou explicite). - Partage d'état en lecture seule ? Rendez-le immuable avec des champs
final; voir Classes immuables. Nivolatileni verrou n'est nécessaire.
Chapitres connexes
- Java
volatile— le mot-clé de visibilité en profondeur. - Synchronisation Java — l'exclusion mutuelle et le verrou moniteur.
- Variables atomiques — opérations atomiques sans verrou.
- Mot-clé
finalet Classes immuables — publication sûre par construction.