Stratégies de Synchronisation en Java pour la Concurrence

Java offre plusieurs mécanismes pour gérer l'accès concurrent aux ressources partagées, garantissant ainsi l'intégrité des données dans un environnement multithreadé.

  1. Verrouillage Pessimiste avec synchronized

Le mot-clé synchronized est une forme de verrouillage pessimiste. Il garantit qu'un seul thread à la fois peut exécuter un bloc de code, une méthode ou accéder à un objet. Chaque objet Java possède un verrou interne. Lorsqu'un thread tente d'acquérir ce verrou pour exécuter une méthode synchronisée, il doit atetndre si un autre thread détient déjà le verrou. Les méthodes synchronisées statiques verrouillent la classe entière.

Avantages : Assure la sérialisation des opérations critiques, prévenant ainsi les données incohérentes dues à des accès concurrents multiples.

Inconvénients : Peut réduire considérablement le débit de traitement en raison de l'attente des threads, et ne fournit pas d'informations sur l'acquisition du verrou.

  1. Synchronisation avec volatile

La spécification volatile pour les variables de champ offre un mécanisme sans verrouillage. Elle impose que chaque accès à une variable volatile se fasse directement depuis la mémoire principale, plutôt que depuis le cache du thread. Cela garantit que tous les threads accèdent à la version la plus récente de la variable.

  • volatile permet aux threads de lire la valeur la plus à jour d'une variable.
  • Il informe la machine virtuelle Java (JVM) que la variable peut être modifiée par d'autres threads, nécessitant une relecture depuis la mémoire.
  • volatile ne garantit pas l'atomicité des opérations composites.
  • Il ne peut pas être utilisé pour modifier des variables final.

L'exemple suivant illustre l'utilisation de volatile. Cependant, en raison de la non-atomicité de l'opération +=, le résultat final peut être inférieur à la valeur attendue.


import java.util.concurrent.atomic.AtomicInteger;

class BankAccount {
    private volatile int balance = 1000; // Utilisation de volatile pour garantir la visibilité

    public int getBalance() {
        return balance;
    }

    public void deposit(int amount) {
        // L'opération d'incrémentation n'est PAS atomique avec volatile seul
        balance += amount;
        System.out.println("Solde après dépôt : " + balance);
    }
}

class BankWorker implements Runnable {
    private BankAccount account;

    public BankWorker(BankAccount account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            account.deposit(100);
            System.out.println(Thread.currentThread().getName() + " - Solde actuel : " + account.getBalance());
            try {
                Thread.sleep(50); // Petite pause pour simuler du travail
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

public class VolatileExample {
    public static void main(String[] args) {
        BankAccount sharedAccount = new BankAccount();
        BankWorker worker = new BankWorker(sharedAccount);

        Thread t1 = new Thread(worker, "Thread-1");
        Thread t2 = new Thread(worker, "Thread-2");

        System.out.println("Démarrage des threads...");
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Dépôt terminé. Solde final attendu : 2000. Solde réel : " + sharedAccount.getBalance());
    }
}

Avantages : Garantit la visibilité des modifications apportées à la variable par différents threads. Offre une alternative légère à synchronized pour des scénarios spécifiques.

Inconvénients : Ne garantit pas l'atomicité des opérations. Ne peut être appliqué qu'aux variables, pas aux blocs de code.

  1. Synchronisasion Atomique avec AtomicInteger

La classe AtomicInteger, faisant partie du package java.util.concurrent.atomic, utilise une combinaison de la technologie CAS (Compare-And-Swap) et de la spécification volatile pour garantir l'atomicité des opérations sans nécessiter de verrouillages explicites comme synchronized.

CAS compare une valeur attendue avec la valeur actuelle. Si elles correspondent, la valeur est mise à jour. Sinon, l'opération échoue et peut être retentée. L'utilisation de volatile assure que la valeur lue est toujours la plus récente.

Avantages : Fournit une atomicité pour les opérations sur un seul champ sans la surcharge des verrous, améliorant considérablement les performances.

Inconvénients : Ne peut garantir l'atomicité que pour les opérations sur le champ lui-même, pas pour des blocs de code plus complexes.


import java.util.concurrent.atomic.AtomicInteger;

class AtomicBankAccount {
    private AtomicInteger balance = new AtomicInteger(1000); // Utilisation de AtomicInteger

    public int getBalance() {
        return balance.get();
    }

    public void deposit(int amount) {
        balance.getAndAdd(amount); // Opération atomique de dépôt
        System.out.println("Solde atomique après dépôt : " + balance.get());
    }
}

class AtomicBankWorker implements Runnable {
    private AtomicBankAccount account;

    public AtomicBankWorker(AtomicBankAccount account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            account.deposit(100);
            System.out.println(Thread.currentThread().getName() + " - Solde atomique actuel : " + account.getBalance());
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

public class AtomicIntegerExample {
    public static void main(String[] args) {
        AtomicBankAccount sharedAccount = new AtomicBankAccount();
        AtomicBankWorker worker = new AtomicBankWorker(sharedAccount);

        Thread t1 = new Thread(worker, "Atomic-Thread-1");
        Thread t2 = new Thread(worker, "Atomic-Thread-2");

        System.out.println("Démarrage des threads atomiques...");
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Dépôt atomique terminé. Solde final attendu : 2000. Solde réel : " + sharedAccount.getBalance());
    }
}

  1. Verrous Réentrables avec ReentrantLock

ReentrantLock est une implémentation plus flexible de verrouillage que synchronized. Elle permet à un thread d'acquérir le même verrou plusieurs fois (réentrance). Son fonctionnement repose sur le mécanisme AQS (AbstractQueuedSynchronizer).

Mécanisme AQS

AQS est un framework de synchronisation qui gère une file d'attente de threads (souvent une file FIFO basée sur CLH - Craig, Landin, and Hayes) et un état (state) représentant la disponibilité de la ressource partagée.

Implémentation de ReentrantLock

ReentrantLock supporte les verrous "justes" (fair) et "injustes" (non-fair).

Verrou Juste (Fair Lock)

Créé avec new ReentrantLock(true). Les threads acquièrent le verrou dans l'ordre où ils l'ont demandé. Un thread en attente est ajouté à la file d'attente. Il ne peut acquérir le verrou que lorsqu'il atteint la tête de la file.

Verrou Injuste (Non-Fair Lock)

Créé avec new ReentrantLock() ou new ReentrantLock(false). Un thread peut tenter d'acquérir le verrou immédiatement, même si d'autres threads attendent. Si le verrou est disponible, il est acquis, potentiellement au détriment des threads déjà en attente. Si le verrou est occupé, le thread est placé dans la file d'attente.

L'acquisition et la libération du verrou se font explicitement avec lock() et unlock(). Il est crucial d'envelopper le code critique dans un bloc try-finally pour garantir la libération du verrou, même en cas d'exception.


import java.util.concurrent.locks.ReentrantLock;

class LockedBankAccount {
    private final ReentrantLock lock = new ReentrantLock(true); // Verrou juste (fair)
    private int balance = 1000;

    public int getBalance() {
        return balance;
    }

    public void deposit(int amount) {
        lock.lock(); // Acquisition du verrou
        try {
            balance += amount;
            System.out.println("Solde avec ReentrantLock après dépôt : " + balance);
        } finally {
            lock.unlock(); // Libération du verrou
        }
    }
}

class LockedBankWorker implements Runnable {
    private LockedBankAccount account;

    public LockedBankWorker(LockedBankAccount account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(30); // Pause pour simuler une opération
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            account.deposit(100);
            System.out.println(Thread.currentThread().getName() + " - Solde actuel avec ReentrantLock : " + account.getBalance());
        }
    }
}

public class ReentrantLockExample {
    public static void main(String[] args) {
        LockedBankAccount sharedAccount = new LockedBankAccount();
        LockedBankWorker worker = new LockedBankWorker(sharedAccount);

        Thread t1 = new Thread(worker, "Reentrant-Thread-1");
        Thread t2 = new Thread(worker, "Reentrant-Thread-2");

        System.out.println("Démarrage des threads avec ReentrantLock...");
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Dépôt avec ReentrantLock terminé. Solde final : " + sharedAccount.getBalance());
    }
}

Étiquettes: Java Concurrence synchronisation volatile atomicinteger

Publié le 9 août à 01h49