Guide de CountDownLatch en Java

Guide de CountDownLatch en Java

1. introduction

Dans cet article, nous allons donner un guide de la classeCountDownLatch et montrer comment elle peut être utilisée dans quelques exemples pratiques.

Essentiellement, en utilisant unCountDownLatch, nous pouvons provoquer le blocage d'un thread jusqu'à ce que d'autres threads aient terminé une tâche donnée.

2. Utilisation dans la programmation simultanée

En termes simples, unCountDownLatch a un champcounter, que vous pouvez décrémenter selon vos besoins. Nous pouvons ensuite l'utiliser pour bloquer un thread appelant jusqu'à ce qu'il soit compté à zéro.

Si nous faisions un traitement parallèle, nous pourrions instancier lesCountDownLatch avec la même valeur pour le compteur que le nombre de threads sur lesquels nous voulons travailler. Ensuite, nous pourrions simplement appelercountdown() après la fin de chaque thread, garantissant qu'un thread dépendant appelantawait() se bloquera jusqu'à ce que les threads de travail soient terminés.

3. En attente d'un pool de threads pour terminer

Essayons ce modèle en créant unWorker et en utilisant un champCountDownLatch pour signaler qu'il est terminé:

public class Worker implements Runnable {
    private List outputScraper;
    private CountDownLatch countDownLatch;

    public Worker(List outputScraper, CountDownLatch countDownLatch) {
        this.outputScraper = outputScraper;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        doSomeWork();
        outputScraper.add("Counted down");
        countDownLatch.countDown();
    }
}

Ensuite, créons un test afin de prouver que nous pouvons amener unCountDownLatch à attendre que les instancesWorker se terminent:

@Test
public void whenParallelProcessing_thenMainThreadWillBlockUntilCompletion()
  throws InterruptedException {

    List outputScraper = Collections.synchronizedList(new ArrayList<>());
    CountDownLatch countDownLatch = new CountDownLatch(5);
    List workers = Stream
      .generate(() -> new Thread(new Worker(outputScraper, countDownLatch)))
      .limit(5)
      .collect(toList());

      workers.forEach(Thread::start);
      countDownLatch.await();
      outputScraper.add("Latch released");

      assertThat(outputScraper)
        .containsExactly(
          "Counted down",
          "Counted down",
          "Counted down",
          "Counted down",
          "Counted down",
          "Latch released"
        );
    }

Naturellement, «Latch relâché» sera toujours la dernière sortie - car il dépend du relâchement deCountDownLatch.

Notez que si nous n’appelions pasawait(), nous ne serions pas en mesure de garantir l’ordre d’exécution des threads, le test échouerait donc de manière aléatoire.

4. A Pool of Threads Waiting to Begin

Si nous prenons l’exemple précédent, mais que cette fois, nous avons démarré des milliers de threads au lieu de cinq, il est probable que beaucoup des premiers auront terminé le traitement avant même d’avoir appeléstart() sur les derniers. Cela pourrait rendre difficile d'essayer de reproduire un problème de concurrence, car nous ne pourrions pas faire fonctionner tous nos threads en parallèle.

Pour contourner ce problème, faisons en sorte que lesCountdownLatch fonctionnent différemment de l'exemple précédent. Au lieu de bloquer un thread parent jusqu'à ce que certains threads soient terminés, nous pouvons bloquer chaque thread enfant jusqu'à ce que tous les autres aient démarré.

Modifions notre méthoderun() pour qu'elle se bloque avant le traitement:

public class WaitingWorker implements Runnable {

    private List outputScraper;
    private CountDownLatch readyThreadCounter;
    private CountDownLatch callingThreadBlocker;
    private CountDownLatch completedThreadCounter;

    public WaitingWorker(
      List outputScraper,
      CountDownLatch readyThreadCounter,
      CountDownLatch callingThreadBlocker,
      CountDownLatch completedThreadCounter) {

        this.outputScraper = outputScraper;
        this.readyThreadCounter = readyThreadCounter;
        this.callingThreadBlocker = callingThreadBlocker;
        this.completedThreadCounter = completedThreadCounter;
    }

    @Override
    public void run() {
        readyThreadCounter.countDown();
        try {
            callingThreadBlocker.await();
            doSomeWork();
            outputScraper.add("Counted down");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            completedThreadCounter.countDown();
        }
    }
}

Maintenant, modifions notre test pour qu'il se bloque jusqu'à ce que tous lesWorkers aient démarré, débloque lesWorkers, puis se bloque jusqu'à ce que lesWorkers soient terminés:

@Test
public void whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime()
 throws InterruptedException {

    List outputScraper = Collections.synchronizedList(new ArrayList<>());
    CountDownLatch readyThreadCounter = new CountDownLatch(5);
    CountDownLatch callingThreadBlocker = new CountDownLatch(1);
    CountDownLatch completedThreadCounter = new CountDownLatch(5);
    List workers = Stream
      .generate(() -> new Thread(new WaitingWorker(
        outputScraper, readyThreadCounter, callingThreadBlocker, completedThreadCounter)))
      .limit(5)
      .collect(toList());

    workers.forEach(Thread::start);
    readyThreadCounter.await();
    outputScraper.add("Workers ready");
    callingThreadBlocker.countDown();
    completedThreadCounter.await();
    outputScraper.add("Workers complete");

    assertThat(outputScraper)
      .containsExactly(
        "Workers ready",
        "Counted down",
        "Counted down",
        "Counted down",
        "Counted down",
        "Counted down",
        "Workers complete"
      );
}

Ce modèle est vraiment utile pour essayer de reproduire des bugs de concurrence, car il peut être utilisé pour forcer des milliers de threads à essayer d’exécuter une logique en parallèle.

5. Terminer unCountdownLatch tôt

Parfois, nous pouvons nous retrouver dans une situation où lesWorkers se terminent par erreur avant de décompter lesCountDownLatch..Cela pourrait entraîner qu'il n'atteigne jamais zéro et queawait() ne se termine jamais:

@Override
public void run() {
    if (true) {
        throw new RuntimeException("Oh dear, I'm a BrokenWorker");
    }
    countDownLatch.countDown();
    outputScraper.add("Counted down");
}

Modifions notre test précédent pour utiliser unBrokenWorker, afin de montrer commentawait() bloquera pour toujours:

@Test
public void whenFailingToParallelProcess_thenMainThreadShouldGetNotGetStuck()
  throws InterruptedException {

    List outputScraper = Collections.synchronizedList(new ArrayList<>());
    CountDownLatch countDownLatch = new CountDownLatch(5);
    List workers = Stream
      .generate(() -> new Thread(new BrokenWorker(outputScraper, countDownLatch)))
      .limit(5)
      .collect(toList());

    workers.forEach(Thread::start);
    countDownLatch.await();
}

Clairement, ce n'est pas le comportement que nous souhaitons - il serait bien mieux pour l'application de continuer que de bloquer à l'infini.

Pour contourner ce problème, ajoutons un argument de délai d'expiration à notre appel àawait().

boolean completed = countDownLatch.await(3L, TimeUnit.SECONDS);
assertThat(completed).isFalse();

Comme nous pouvons le voir, le test finira par expirer etawait() renverrafalse.

6. Conclusion

Dans ce guide rapide, nous avons montré comment nous pouvons utiliser unCountDownLatch pour bloquer un thread jusqu'à ce que d'autres threads aient terminé leur traitement.

Nous avons également montré comment il peut être utilisé pour résoudre les problèmes de concurrence en s'assurant que les threads s'exécutent en parallèle.

L'implémentation de ces exemples peut être trouvéeover on GitHub; il s'agit d'un projet basé sur Maven, il devrait donc être facile à exécuter tel quel.