Le Modèle de Mémoire en Java : Résolution des Problèmes de Visibilité et d'Ordre

En programmation concurrente, les problèmes de visibilité et d'ordre surviennent à cause des mécanismes de cache processeur et des optimisations du compilateur. La sloution initiale serait de désactiver ces caches et optimisations, mais cela nuirait gravement aux performances. Une approche plus judicieuse consiste à les désactiver de manière sélective selon les besoins du programmeur.

Le modèle de mémoire en Java définit comment la JVM fournit des mécanismes pour cette désactivation sélective. Il repose sur des mots-clés tels que volatile, synchronized, final, ainsi que sur six règles Happens-Before qui établissent des garanties d'ordre et de visibilité entre opérations.

Le mot-clé volatile, présent aussi dans des langages comme C, force les accès à une variable à contourner le cache processeur. Par exemple, déclarer volatile int compteur = 0; signifie que toute lecture ou écriture de compteur doit passer par la mémoire principale, assurrant ainsi la visibilité entre threads.

Considérons un exemple illustrant l'effet de volatile sur la visibilité :

class ExempleSynchro {
    int donnee = 0;
    volatile boolean drapeau = false;
    public void ecrireDonnee() {
        donnee = 99;
        drapeau = true;
    }
    public void lireDonnee() {
        if (drapeau) {
            // À ce stade, donnee est nécessairement 99 dans les versions modernes de Java
        }
    }
}

Dans les versions antérieures à Java 1.5, un thread lisant drapeau à true pourrait encore voir donnee à 0 en raison de problèmes de cache. Depuis Java 1.5, le modèle de mémoire a été renforcé grâce aux règles Happens-Before, garantissant que l'écriture de donnee est visible avant la lecture de drapeau.

Les règles Happens-Before assurent qu'une opération antérieure est visible pour une opération ultérieure. Voici les règles clés :

  • Règle d'ordre programmatique : Dans un même thread, les opérations sont visibles dans l'ordre du code. Ainsi, dans l'exemple ci-dessus, l'écriture de donnee precede visuellement l'écriture de drapeau.
  • Règle pour les variables volatile : Une écriture sur une variable volatile est visible avant toute lecture ultérieure de cette même variable.
  • Transitivité : Si l'opération A précède l'opération B, et que B précède C, alors A précède C. Dans l'exemple, l'écriture de donnee précède l'écriture de drapeau, et l'écriture de drapeau précède sa lecture, donc l'écriture de donnee précède la lecture de drapeau.
  • Règle pour les verrous moniteurs : La libération d'un verrou est visible avant tout verrouillage ultérieur du même verrou. Par exemple :
synchronized (this) {
    // Bloc synchronisé : les modifications sont visibles après déverrouillage
    if (this.valeur < 10) {
        this.valeur = 10;
    }
}

Après qu'un thread a libéré le verrou, un autre thread qui acquiert le verrou verra les modifications effectuées.

  • Règle pour le démarrage de thread : Lorsqu'un thread principal démarre un thread enfant via start(), toutes les opérations effectuées avant le démarrage sont visibles pour le thread enfant.
Thread enfant = new Thread(() -> {
    // Les modifications apportées avant start() sont visibles ici
});
variablePartagee = 42;
enfant.start();
  • Règle pour l'attente de thread : Lorsqu'un thread principal attend la fin d'un thread enfant via join(), toutes les opérations du thread enfant sont visibles pour le thread principal après l'appel à join().
Thread enfant = new Thread(() -> {
    variablePartagee = 55;
});
enfant.start();
enfant.join();
// À ce stade, variablePartagee est 55

Le mot-clé final permet d'optimiser les variables immuables, mais des problèmes de visibilité peuvent survenir si l'objet est exposé prématurément (phénomène d'évasion). Par exemple, dans un constructeur, affecter this à une variable globale avant l'initialisation complète peut causer des incohérences. Ainsi, il faut éviter l'évasion pour garantir que les champs final sont correctement visibles.

class FinalDemo {
    final int id;
    public FinalDemo() {
        id = 10;
        // Évasion incorrecte : exposition de 'this' avant fin de construction
        registreGlobal = this;
    }
}

En plus de volatile, d'autres mécanismes comme les blocs synchronized ou l'utilisation de join() peuvent assurer la visibilité des modifications entre threads.

Étiquettes: Java MemoireVolatile synchronized final Happens-Before

Publié le 2 juin à 03h05