CodeGym /Cours /JAVA 25 SELF /synchronized, volatile : syntaxe, utilisation

synchronized, volatile : syntaxe, utilisation

JAVA 25 SELF
Niveau 52 , Leçon 1
Disponible

1. Le mot-clé synchronized : pourquoi et comment

En Java, le mot-clé synchronized — c’est comme un écriteau « Occupé ! » sur la porte des toilettes : tant qu’un thread se trouve dans la « section critique », les autres attendent poliment leur tour. Ce n’est que lorsque le premier sort que le suivant peut entrer et exécuter son code.

Syntaxe : bloc et méthode

Bloc synchronisé

synchronized (object) {
    // section critique
}
  • object — c’est n’importe quel objet sur lequel vous voulez « poser un verrou ». Tant qu’un thread exécute ce bloc, les autres threads qui veulent aussi entrer dans un bloc avec ce même objet devront attendre.

Méthode synchronisée

public synchronized void increment() {
    // section critique
}
  • Ici, le « verrou » est posé sur l’objet lui-même (this). Autrement dit, un seul thread à la fois peut exécuter n’importe quelle méthode synchronisée de cet objet.

Méthode statique synchronisée

public static synchronized void foo() {
    // section critique
}
  • Ici, le verrouillage a lieu au niveau de la classe (ClassName.class), et non d’un objet particulier.

Comment cela fonctionne sous le capot

Lorsqu’un thread entre dans un bloc ou une méthode synchronisé(e), il acquiert le « moniteur » de l’objet (ou de la classe pour les méthodes statiques). Si le moniteur est déjà occupé — le thread attend. Dès que le moniteur est libéré, le thread suivant peut entrer.

2. Exemple : incrément d’un compteur avec et sans synchronisation

Sans synchronisation

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
public class CounterDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Valeur finale: " + counter.getCount());
    }
}

Valeur attendue : 2000
Valeur réelle : peut être inférieure (par exemple, 1995, 1987…), et à chaque exécution — sa propre « surprise ».

Pourquoi ? Parce que l’opération count++ n’est pas atomique : elle est décomposée en trois actions — lire la valeur, l’incrémenter, l’écrire à nouveau. Si deux threads font cela simultanément, ils peuvent « écraser » le résultat l’un de l’autre.

Solution : synchronized

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

Désormais, un seul thread à la fois peut exécuter la méthode increment(). La valeur finale sera toujours 2000.

Alternative : bloc synchronisé

public class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }
}

Le résultat sera le même. Vous pouvez synchroniser non pas toute la méthode, mais uniquement la partie nécessaire.

3. Introduction au « moniteur d’objet »

Le moniteur est un « verrou » intégré à chaque objet en Java. Lorsque vous écrivez synchronized(object), le thread essaie de « verrouiller » cet objet. Si le verrou est libre — le thread l’obtient, sinon — il attend son tour. Dès que le thread sort du bloc, le verrou est libéré.

Important ! Si vous vous synchronisez sur des objets différents — les threads ne s’attendront pas mutuellement. Il est donc très important de choisir le bon objet pour la synchronisation.

Méthodes statiques synchronisées

Parfois, la ressource partagée n’est pas un objet, mais quelque chose de commun à toutes les instances de la classe (par exemple, une variable statique). Dans ce cas, la synchronisation doit se faire au niveau de la classe.

public class StaticCounter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

Équivalent à :

public static void increment() {
    synchronized (StaticCounter.class) {
        count++;
    }
}

Le moniteur est attaché à l’objet de classe (Class), et non à une instance concrète.

4. Le mot-clé volatile : qu’est-ce que c’est et à quoi sert-il

Problème de visibilité entre threads

En Java, chaque thread peut mettre en cache des valeurs de variables pour accélérer l’exécution. Cela signifie que si un thread modifie une variable, un autre thread peut « ne pas le remarquer » et continuer à lire la valeur depuis son cache local. C’est particulièrement critique pour les drapeaux par lesquels les threads se signalent des événements.

Comment fonctionne volatile

Si une variable est déclarée volatile, cela signifie :

  • Tous les threads la lisent et l’écrivent toujours en mémoire principale, en contournant le cache.
  • Toute modification de la variable devient immédiatement visible pour tous les threads.

Mais ! Les opérations avec volatile ne sont pas atomiques en elles-mêmes (sauf la lecture/écriture simple de types primitifs comme boolean, int, etc.). Si vous faites quelque chose de plus complexe qu’une affectation — une synchronisation est nécessaire.

Exemple : drapeau d’arrêt

public class Worker extends Thread {
    private volatile boolean running = true;

    public void run() {
        while (running) {
            // on fait quelque chose d'utile
        }
        System.out.println("Thread terminé");
    }

    public void shutdown() {
        running = false;
    }
}
Worker w = new Worker();
w.start();
// ... après un certain temps
w.shutdown();

Sans volatile, le thread peut « ne pas remarquer » le changement du drapeau et boucler indéfiniment (surtout sur des systèmes multi-cœurs). Avec volatile — tout fonctionne comme il faut.

5. Limites de volatile : non-atomicité

Beaucoup de débutants pensent : « Si je mets volatile sur un int, je peux écrire count++ sans me soucier. » Hélas, ce n’est pas le cas :

private volatile int count = 0;

public void increment() {
    count++;
}

Erreur ! L’opération count++ n’est toujours pas atomique — c’est trois étapes : (1) lire, (2) incrémenter, (3) écrire. Si deux threads lisent en même temps la même valeur, l’incrémentent tous les deux et écrivent tous les deux le même résultat — un incrément « se perdra ».

Conclusion : volatile garantit uniquement la visibilité des changements, mais ne protège pas des conditions de course lors d’opérations complexes.

6. Quand utiliser synchronized et quand — volatile

  • volatile — lorsqu’il s’agit d’un simple drapeau (par exemple, boolean) qu’un thread écrit et qu’un autre lit. Exemple : arrêt d’un thread, signalisation d’un événement.
  • synchronized — lorsque vous devez garantir l’atomicité d’opérations complexes (par exemple, incrément, modification de plusieurs variables, manipulation de structures de données).

Tableau mémo

Scénario volatile synchronized
Transmettre un signal entre threads
Opération atomique (incrément)
Étapes multiples dans une section critique
Visibilité seule des changements

7. Erreurs typiques avec synchronized et volatile

Erreur n° 1 : synchronisation sur le mauvais objet. Si vous vous synchronisez sur une variable locale ou sur un objet différent dans chaque thread — aucune protection ne fonctionnera.

Object lock = new Object();
synchronized (lock) {
    // ...
}

Si chaque thread crée son propre lock — cela ne sert à rien. Il faut un point de synchronisation unique, un objet commun à tous les threads.

Erreur n° 2 : attendre l’atomicité de volatile. volatile garantit la visibilité, pas l’atomicité. Les opérations comme count++ restent dangereuses sans synchronisation.

Erreur n° 3 : synchroniser une zone de code trop large. Si vous synchronisez toute la méthode alors qu’il ne faut qu’une seule ligne, vous bloquez inutilement les autres threads et perdez en performance. Essayez de réduire la « section critique ».

Erreur n° 4 : oublier de rendre la synchronisation « statique » pour des données statiques. Si vous avez une variable statique et que vous vous synchronisez sur this, cela n’aidera pas. Pour les données statiques, il faut une synchronisation au niveau de la classe : synchronized(ClassName.class).

Erreur n° 5 : se synchroniser sur un littéral de chaîne. La synchronisation sur des chaînes est dangereuse, car les littéraux identiques sont internés par la JVM. On peut accidentellement obtenir un verrou partagé pour différentes parties du programme.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION