Contrôle de synchronisation concurrente en Java

Les mécanismes de synchronisation sont essentiels pour gérer l'accès concurrent aux ressources partagées. Cet article explore plusieurs outils de synchronisation en Java, en insistant sur leur implémentation et leurs cas d'utilistaion spécifiques.

Serrure réentrante ReentrantLock

Cette classe offre un contrôle manuel des verrous avec plus de flexibilité que le mot-clé synchronized. Les threads peuvent interrompre leur attente de verrouillage et les verrous peuvent être équitables (fair) pour éviter la famine des threads.

public class ResourceAccessor implements Runnable {
    private static final ReentrantLock verrou = new ReentrantLock();
    private static int compteur = 0;

    @Override
    public void run() {
        for (int i = 0; i < 50000; i++) {
            verrou.lock();
            try {
                compteur++;
            } finally {
                verrou.unlock();
            }
        }
    }
}

La méthode lockInterruptibly() permet aux threads de répondre aux interruptions pendant l'attente du verrou. tryLock() tente d'acquérir le verrou sans bloquer, tandis que tryLock(timeout) attend pendant une durée spécifiée.

public class InterruptionDemo implements Runnable {
    private static final ReentrantLock premierVerrou = new ReentrantLock();
    private static final ReentrantLock secondVerrou = new ReentrantLock();
    private final int choix;

    public InterruptionDemo(int choix) {
        this.choix = choix;
    }

    @Override
    public void run() {
        try {
            if (choix == 1) {
                premierVerrou.lockInterruptibly();
                Thread.sleep(200);
                secondVerrou.lockInterruptibly();
            } else {
                secondVerrou.lockInterruptibly();
                Thread.sleep(200);
                premierVerrou.lockInterruptibly();
            }
            // Traitement...
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getId() + " interrompu");
        } finally {
            if (premierVerrou.isHeldByCurrentThread()) premierVerrou.unlock();
            if (secondVerrou.isHeldByCurrentThread()) secondVerrou.unlock();
        }
    }
}

Les verrous réentrants permettent aux threads d'acquérir plusieurs fois le même verrou, à condition de le libérer le même nombre de fois. La version équitable (new ReentrantLock(true)) garantit un ordre FIFO mais avec des performances réduites.

Conditions de synchronisation

La classe Condition est associée à un ReentrantLock et permet aux threads d'attendre des conditions spécifiques. Les méthodes await() et signal() remplacent avantageusement wait()/notify() avec un contrôle plus précis.

public class FileDeCommande {
    private final ReentrantLock verrou = new ReentrantLock();
    private final Condition nonVide = verrou.newCondition();
    private final LinkedList<Tache> file = new LinkedList<>();

    public void ajouterTache(Tache t) {
        verrou.lock();
        try {
            file.addLast(t);
            nonVide.signal();
        } finally {
            verrou.unlock();
        }
    }

    public Tache retirerTache() throws InterruptedException {
        verrou.lock();
        try {
            while (file.isEmpty()) {
                nonVide.await();
            }
            return file.removeFirst();
        } finally {
            verrou.unlock();
        }
    }
}

Sémaphore

Le sémaphore étend le concept de verrou pour permettre l'accès à une ressource à plusieurs threads simultanément. Il est initialisé avec un nombre de permissions.

Semaphore semaphore = new Semaphore(3); // 3 accès simultanés

public void utiliserRessource() throws InterruptedException {
    semaphore.acquire();
    try {
        // Accès à la ressource limitée
    } finally {
        semaphore.release();
    }
}

Verrou en lecture-écriture

ReentrantReadWriteLock sépare les opérations en lcetures (concurrentes) et écritures (exclusives). Les lectures peuvent s'exécuter simultanément tant qu'aucune écriture n'est en cours.

public class CachePartage {
    private final ReentrantReadWriteLock verrouLE = new ReentrantReadWriteLock();
    private final Map<String, Donnees> cache = new HashMap<>();

    public Donnees lire(String cle) {
        verrouLE.readLock().lock();
        try {
            return cache.get(cle);
        } finally {
            verrouLE.readLock().unlock();
        }
    }

    public void ecrire(String cle, Donnees valeur) {
        verrouLE.writeLock().lock();
        try {
            cache.put(cle, valeur);
        } finally {
            verrouLE.writeLock().unlock();
        }
    }
}

Compte à rebours CountDownLatch

Cet outil permet à un ou plusieurs threads d'attendre qu'un ensemble d'opérations soit terminé. Le compteur ne peut être réinitialisé.

public class DemarrageCoordonne {
    private static final CountDownLatch verrou = new CountDownLatch(3);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 3; i++) {
            service.submit(() -> {
                preparerComposant();
                verrou.countDown();
            });
        }
        verrou.await();
        demarrerSysteme();
        service.shutdown();
    }
}

Barrière cyclique CyclicBarrier

Contrairement au CountDownLatch, la barrière cyclique peut être réutilisée après chaque franchissement. Un action optionnelle peut être exécutée lorsque tous les threads arrivent à la barrière.

public class TraitementParLot {
    private final CyclicBarrier barriere;
    private final List<String> donnees;

    public TraitementParLot(List<String> donnees) {
        this.donnees = donnees;
        this.barriere = new CyclicBarrier(4, this::traiterResultats);
    }

    public void executer() {
        // Chaque thread traite une portion des données
        barriere.await();
        // Les résultats sont combinés automatiquement
    }
}

LockSupport

Cette classe fournit des méthodes park() et unpark() pour bloquer et débloquer des threads de manière fiable. Contrairement à Thread.suspend(), elle évite les problèmes d'état inconsistent.

public class ControleFinGrain {
    private final Thread worker;

    public ControleFinGrain(Runnable tache) {
        worker = new Thread(() -> {
            LockSupport.park(); // Attente du signal
            tache.run();
        });
    }

    public void demarrer() {
        worker.start();
    }

    public void autoriserExecution() {
        LockSupport.unpark(worker);
    }
}

Algorithmes de limitation de débit

L'algorithme du seau percé (leaky bucket) régule le trafic en le stockant temporairement avant de le traiter à un rythme constant. L'algorithme du seau à jetons (token bucket) génère des jetons périodiquement qui sont consommés par les requêtes entrantes.

Étiquettes: ReentrantLock Condition Semaphore ReadWriteLock CountDownLatch

Publié le 11 juillet à 18h01