Java Multithreading avec CountDownLatch : Guide Complet de Synchronisation

Maitrisez la synchronisation des threads en Java avec CountDownLatch. Exemples pratiques, bonnes pratiques et pieges courants pour eviter les deadlocks et creer des applications concurrentes robustes.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 7 min read
Java Multithreading avec CountDownLatch : Guide Complet de Synchronisation

Synchronisation et Programmation Multithreadee en Java : Guide Complet avec CountDownLatch

Introduction

La programmation multithreadee est une technique fondamentale en Java qui permet a plusieurs threads de s’executer simultanement au sein d’un meme processus. Cette approche est essentielle pour tirer parti des processeurs multi-coeurs modernes et ameliorer significativement les performances des applications.

Dans le monde reel, de nombreuses situations necessitent l’execution parallele de taches : le traitement de multiples requetes HTTP sur un serveur web, le telechargement simultane de fichiers, ou encore l’execution de calculs intensifs repartis sur plusieurs coeurs. Java offre un ecosysteme riche pour gerer ces scenarios grace a son package java.util.concurrent.

Cependant, la programmation concurrente introduit des defis complexes : les race conditions, les deadlocks, et les problemes de visibilite memoire. La synchronisation devient alors cruciale pour garantir la coherence des donnees partagees entre threads.

Dans cet article, nous explorerons en profondeur :

  • Les concepts fondamentaux des threads en Java
  • Les mecanismes de synchronisation avec le mot-cle synchronized
  • L’utilisation avancee de CountDownLatch pour coordonner les threads
  • Les bonnes pratiques et les pieges courants a eviter

Comprendre les Concepts de Base

Avant de plonger dans la programmation multithreadee, il est essentiel de comprendre les concepts fondamentaux. Un thread (ou fil d’execution) est une unite d’execution legere qui partage l’espace memoire du processus parent tout en maintenant sa propre pile d’appels.

En Java, il existe deux facons principales de creer un thread :

  1. Implementer l’interface Runnable (recommande)
  2. Etendre la classe Thread (moins flexible)

Exemple de Thread avec Runnable

L’approche recommandee consiste a implementer l’interface Runnable et definir la methode run() :

public class HelloThread implements Runnable {
    private final String message;

    public HelloThread(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println("[" + threadName + "] " + message);
    }
}

Pour lancer le thread, nous utilisons la methode start() sur un objet Thread :

public static void main(String[] args) {
    // Creation de plusieurs threads
    for (int i = 1; i <= 3; i++) {
        HelloThread task = new HelloThread("Message " + i);
        Thread thread = new Thread(task, "Thread-" + i);
        thread.start();
    }

    System.out.println("[Main] Tous les threads ont ete lances");
}

Sortie possible (l’ordre peut varier) :

[Main] Tous les threads ont ete lances
[Thread-2] Message 2
[Thread-1] Message 1
[Thread-3] Message 3

Utilisation des Expressions Lambda (Java 8+)

Depuis Java 8, vous pouvez simplifier la creation de threads avec les lambdas :

public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        System.out.println("Execution dans un thread separe");
        // Logique du thread ici
    });
    thread.start();
}

Synchronisation

La synchronisation est un concept crucial en programmation multithreadee. Elle consiste a controler l’acces aux ressources partagees pour eviter les race conditions (conditions de concurrence) qui peuvent mener a des resultats imprevisibles.

Le Probleme des Race Conditions

Sans synchronisation, plusieurs threads peuvent modifier une variable simultanement, causant des pertes de donnees :

// PROBLEME : Race condition sans synchronisation
public class CompteurNonSecurise {
    private int valeur = 0;

    public void increment() {
        valeur++;  // N'est PAS atomique ! (lecture + modification + ecriture)
    }

    public int getValeur() {
        return valeur;
    }
}

Solution avec synchronized

Le mot-cle synchronized garantit qu’un seul thread a la fois peut executer une methode ou un bloc de code :

public class CompteurSecurise {
    private int valeur = 0;

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

    public synchronized int getValeur() {
        return valeur;
    }
}

Bloc Synchronise pour un Controle Plus Fin

Vous pouvez egalement synchroniser uniquement une partie du code :

public class CompteurOptimise {
    private int valeur = 0;
    private final Object lock = new Object();

    public void increment() {
        // Code non synchronise (plus rapide)
        doSomePreparation();

        // Seulement cette section est synchronisee
        synchronized (lock) {
            valeur++;
        }

        // Code non synchronise
        doSomeCleanup();
    }

    public int getValeur() {
        synchronized (lock) {
            return valeur;
        }
    }

    private void doSomePreparation() { /* ... */ }
    private void doSomeCleanup() { /* ... */ }
}

CountDownLatch : Coordination Avancee des Threads

CountDownLatch est une classe puissante du package java.util.concurrent qui permet a un ou plusieurs threads d’attendre que d’autres threads aient termine leurs taches avant de poursuivre.

Fonctionnement de CountDownLatch

Le principe est simple :

  1. Initialiser le latch avec un compte (nombre de taches a attendre)
  2. Chaque thread appelle countDown() lorsqu’il termine sa tache
  3. Le(s) thread(s) en attente appelle(nt) await() pour bloquer jusqu’a ce que le compte atteigne zero

Exemple Complet avec CountDownLatch

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        int numberOfWorkers = 5;
        CountDownLatch latch = new CountDownLatch(numberOfWorkers);

        System.out.println("[Main] Demarrage de " + numberOfWorkers + " workers...");

        for (int i = 1; i <= numberOfWorkers; i++) {
            Thread worker = new Thread(new Worker(latch, i));
            worker.start();
        }

        // Le thread principal attend que tous les workers terminent
        latch.await();

        System.out.println("[Main] Tous les workers ont termine !");
        System.out.println("[Main] Traitement final en cours...");
    }
}

class Worker implements Runnable {
    private final CountDownLatch latch;
    private final int workerId;

    public Worker(CountDownLatch latch, int workerId) {
        this.latch = latch;
        this.workerId = workerId;
    }

    @Override
    public void run() {
        try {
            // Simulation d'un travail de duree variable
            int duration = (int) (Math.random() * 3000) + 1000;
            System.out.println("[Worker-" + workerId + "] Debut du travail (" + duration + "ms)");
            Thread.sleep(duration);
            System.out.println("[Worker-" + workerId + "] Travail termine");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // IMPORTANT : toujours appeler countDown(), meme en cas d'erreur
            latch.countDown();
        }
    }
}

Sortie typique :

[Main] Demarrage de 5 workers...
[Worker-1] Debut du travail (1500ms)
[Worker-3] Debut du travail (2200ms)
[Worker-2] Debut du travail (1100ms)
[Worker-4] Debut du travail (2800ms)
[Worker-5] Debut du travail (1800ms)
[Worker-2] Travail termine
[Worker-1] Travail termine
[Worker-5] Travail termine
[Worker-3] Travail termine
[Worker-4] Travail termine
[Main] Tous les workers ont termine !
[Main] Traitement final en cours...

CountDownLatch avec Timeout

Vous pouvez limiter le temps d’attente avec await(timeout, unit) :

import java.util.concurrent.TimeUnit;

// Attendre maximum 10 secondes
boolean completed = latch.await(10, TimeUnit.SECONDS);

if (completed) {
    System.out.println("Tous les workers ont termine a temps");
} else {
    System.out.println("Timeout ! Certains workers n'ont pas termine");
}

Bonnes Pratiques

Pour ecrire du code multithreade robuste et maintenable, suivez ces recommandations :

1. Toujours Appeler countDown() dans un Bloc Finally

try {
    // Travail du thread
    doWork();
} catch (Exception e) {
    handleError(e);
} finally {
    // Garantit que le latch est decremente meme en cas d'erreur
    latch.countDown();
}

2. Preferer les Classes Atomiques pour les Operations Simples

import java.util.concurrent.atomic.AtomicInteger;

public class CompteurAtomique {
    private final AtomicInteger valeur = new AtomicInteger(0);

    public void increment() {
        valeur.incrementAndGet();  // Thread-safe sans synchronized
    }

    public int getValeur() {
        return valeur.get();
    }
}

3. Utiliser ExecutorService au Lieu de Creer des Threads Manuellement

import java.util.concurrent.*;

public class ExecutorServiceExample {
    public static void main(String[] args) throws InterruptedException {
        int numberOfTasks = 10;
        CountDownLatch latch = new CountDownLatch(numberOfTasks);

        // Pool de threads reutilisable
        ExecutorService executor = Executors.newFixedThreadPool(4);

        for (int i = 0; i < numberOfTasks; i++) {
            final int taskId = i;
            executor.submit(() -> {
                try {
                    System.out.println("Tache " + taskId + " en cours...");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        executor.shutdown();  // Important : toujours fermer l'executor
        System.out.println("Toutes les taches terminees");
    }
}

4. Toujours Definir un Timeout pour await()

// Evite les blocages indefinis
if (!latch.await(30, TimeUnit.SECONDS)) {
    throw new TimeoutException("Les workers n'ont pas termine dans le delai imparti");
}

Pieges Courants

1. Oublier d’Appeler countDown()

Probleme : Le thread principal reste bloque indefiniment.

// MAUVAIS : countDown() n'est jamais appele si une exception survient
public void run() {
    doWork();  // Si exception ici, countDown() n'est pas appele
    latch.countDown();
}

// BON : countDown() est toujours appele
public void run() {
    try {
        doWork();
    } finally {
        latch.countDown();
    }
}

2. Reutiliser un CountDownLatch

Probleme : CountDownLatch ne peut pas etre reinitialise.

CountDownLatch latch = new CountDownLatch(3);
// Apres que le compte atteigne 0, le latch ne peut pas etre reutilise

// Pour des scenarios repetables, utilisez CyclicBarrier a la place
CyclicBarrier barrier = new CyclicBarrier(3);

3. Ne Pas Gerer InterruptedException Correctement

// MAUVAIS : Avaler l'exception sans restaurer le statut d'interruption
try {
    latch.await();
} catch (InterruptedException e) {
    // Ne rien faire = MAUVAIS
}

// BON : Restaurer le statut d'interruption
try {
    latch.await();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();  // Restaure le flag
    throw new RuntimeException("Attente interrompue", e);
}

4. Deadlock avec Synchronisation Imbriquee

// DANGER : Peut causer un deadlock
public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            synchronized (lock2) {
                // ...
            }
        }
    }

    public void method2() {
        synchronized (lock2) {  // Ordre inverse = DEADLOCK potentiel
            synchronized (lock1) {
                // ...
            }
        }
    }
}

Solution : Toujours acquerir les locks dans le meme ordre.

Conclusion

La programmation multithreadee en Java est un outil puissant pour creer des applications performantes et reactives. CountDownLatch est particulierement utile pour :

  • Initialisation parallele : Demarrer plusieurs services et attendre qu’ils soient tous prets
  • Traitement par lots : Diviser un gros travail en taches paralleles
  • Tests de concurrence : Synchroniser le demarrage de plusieurs threads de test

Les points cles a retenir :

  1. Synchronisation : Utilisez synchronized ou les classes atomiques pour proteger les ressources partagees
  2. CountDownLatch : Ideal pour attendre que N taches soient terminees
  3. Gestion des erreurs : Toujours appeler countDown() dans un bloc finally
  4. Timeouts : Definir des limites de temps pour eviter les blocages indefinis
  5. ExecutorService : Preferer les pools de threads aux threads manuels

Prochaines Etapes

Pour approfondir vos connaissances en programmation concurrente Java :

  • Explorez les autres primitives : CyclicBarrier, Semaphore, Phaser
  • Maitrisez les Locks : ReentrantLock, ReadWriteLock pour un controle plus fin
  • Etudiez CompletableFuture : Programmation asynchrone moderne en Java 8+
  • Decouvrez les Collections Concurrentes : ConcurrentHashMap, BlockingQueue
  • Testez votre code : Utilisez des outils comme jcstress pour detecter les race conditions
Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles