W3docs

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 unseen

Le 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ègleArc happens-before
Ordre du programmeChaque action dans un thread se produit avant les actions ultérieures dans ce même thread
Verrou moniteurLe déverrouillage d'un moniteur se produit avant le verrouillage ultérieur du même moniteur
VolatileUne écriture dans un champ volatile se produit avant toute lecture ultérieure de ce champ
Démarrage de threadthread.start() se produit avant toute action dans le thread démarré
Jointure de threadToutes les actions d'un thread se produisent avant qu'un autre thread retourne de son join()
Champs finalLes é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 here

Atomicité 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èmeSymptômeCorrection
VisibilitéUn thread ne voit jamais une valeur mise à jourvolatile, synchronized, final
AtomicitéMises à jour perdues avec x++ sous contentionsynchronized, AtomicInteger, verrous
RéordonnancementLes opérations apparaissent dans le mauvais ordrehappens-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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • reader saw data = 42 prouve que l'écriture volatile dans flag a publié l'écriture ordinaire dans data — le lecteur est garanti de la voir grâce à l'arc happens-before.
  • atomic counter = 800000 (expected 800000) montre que AtomicInteger.incrementAndGet() n'a perdu aucune mise à jour parmi 8 threads effectuant chacun 100 000 incréments — un int++ ordinaire afficherait un nombre plus petit et non déterministe.
  • final config = prod:443 démontre la publication sûre : les champs final de Config sont corrects sans aucun volatile ni verrou.
  • synchronized sum = 10000 confirme 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. Ni volatile ni verrou n'est nécessaire.

Chapitres connexes

Pratique

Pratique
Quelle garantie offre la déclaration d'un champ 'volatile' dans le modèle mémoire Java ?
Quelle garantie offre la déclaration d'un champ 'volatile' dans le modèle mémoire Java ?
Was this page helpful?