Problèmes de synchronisation sur un champ non final en Java

Lors de l'utilisation du mot-clé synchronized en Java, les environnements de développement intégrés comme IntelliJ IDEA affichent souvent un avertissement indiquant "Synchronization on a non-final field". Cet avertissement met en lumière un piège courant en programmation concurrente qui peut compromettre l'intégrité des sections critiques.

Analyse du problème

Le mécanisme de verrouillage en Java repose sur l'instance de l'objet utilisé comme moniteur. Si la référence de cet objet est modifiée pendant l'exécution, les threads suivants aqcuerront un verrou différent. Par conséquent, l'exclusion mutuelle est brisée, permettant à pluseiurs threads d'exécuter simultanément le bloc synchronisé. L'utilisation du modificateur final garantit que la référence de l'objet de verrouillage reste immuable.

Démonstration de l'erreur

Considérons l'implémentation suivante où la référence du moniteur est réassignée à l'intérieur du bloc synchronisé :

public class UnsafeSynchronization implements Runnable {
    private Object monitor = new Object();

    @Override
    public void run() {
        synchronized (monitor) {
            System.out.println(Thread.currentThread().getName() + " a acquis le verrou.");
            
            // Réassignation de la référence, ce qui brise la synchronisation
            monitor = new Object();
            
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() + " a libéré le verrou.");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        UnsafeSynchronization task = new UnsafeSynchronization();
        new Thread(task, "Thread-A").start();
        Thread.sleep(100);
        new Thread(task, "Thread-B").start();
    }
}

Dans ce scénario, le deuxième thread n'attend pas que le premier ait terminé son exécution. La sortie console ressemblera à ceci :

Thread-A a acquis le verrou.
Thread-B a acquis le verrou.
Thread-A a libéré le verrou.
Thread-B a libéré le verrou.

Les deux threads entrent dans la section critique presque simultanément car Thread-B se syncrhonise sur la nouvelle instance de monitor créée par Thread-A.

Implémentation correcte

Pour résoudre ce problème, il est impératif de déclarer l'objet de verrouillage comme final. Cela empêche toute réassignation accidentelle de la référence, assurant ainsi que tous les threads partagent le même moniteur.

public class SafeSynchronization implements Runnable {
    private final Object monitor = new Object();

    @Override
    public void run() {
        synchronized (monitor) {
            System.out.println(Thread.currentThread().getName() + " a acquis le verrou.");
            
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() + " a libéré le verrou.");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SafeSynchronization task = new SafeSynchronization();
        new Thread(task, "Thread-A").start();
        Thread.sleep(100);
        new Thread(task, "Thread-B").start();
    }
}

Avec cette correction, la sortie console démontre un comportement séquentiel correct :

Thread-A a acquis le verrou.
Thread-A a libéré le verrou.
Thread-B a acquis le verrou.
Thread-B a libéré le verrou.

Étiquettes: Java synchronized multithreading Concurrency Thread-Safety

Publié le 7 juillet à 04h07