Leitfaden zu CountDownLatch in Java

Anleitung zu CountDownLatch in Java

1. Einführung

In diesem Artikel geben wir einen Leitfaden für die KlasseCountDownLatchund zeigen anhand einiger praktischer Beispiele, wie sie verwendet werden kann.

Im Wesentlichen können wir durch die Verwendung vonCountDownLatch bewirken, dass ein Thread blockiert wird, bis andere Threads eine bestimmte Aufgabe abgeschlossen haben.

2. Verwendung in der gleichzeitigen Programmierung

Einfach ausgedrückt, einCountDownLatch hat eincounter-Feld, das Sie nach Bedarf dekrementieren können. Wir können es dann verwenden, um einen aufrufenden Thread zu blockieren, bis er auf Null heruntergezählt wurde.

Wenn wir eine parallele Verarbeitung durchführen würden, könnten wir dieCountDownLatch mit demselben Wert für den Zähler instanziieren wie eine Anzahl von Threads, über die wir arbeiten möchten. Dann könnten wir einfachcountdown() aufrufen, nachdem jeder Thread beendet wurde, um sicherzustellen, dass ein abhängiger Thread, derawait() aufruft, blockiert, bis die Arbeitsthreads beendet sind.

3. Warten auf den Abschluss eines Thread-Pools

Probieren Sie dieses Muster aus, indem Sie einWorker erstellen und einCountDownLatch-Feld verwenden, um zu signalisieren, wann es abgeschlossen ist:

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();
    }
}

Erstellen wir dann einen Test, um zu beweisen, dass wirCountDownLatch erhalten können, um auf den Abschluss derWorker-Instanzen zu warten:

@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"
        );
    }

Natürlich ist "Latch freigegeben" immer die letzte Ausgabe - da dies von der Freigabe vonCountDownLatchabhängt.

Beachten Sie, dass wir die Reihenfolge der Ausführung der Threads nicht garantieren können, wenn wirawait() nicht aufrufen, sodass der Test zufällig fehlschlägt.

4. A Pool of Threads Waiting to Begin

Wenn wir das vorherige Beispiel genommen haben, diesmal jedoch Tausende von Threads anstelle von fünf gestartet haben, ist es wahrscheinlich, dass viele der früheren Threads die Verarbeitung abgeschlossen haben, bevor wir bei den späteren sogarstart() aufgerufen haben. Dies könnte es schwierig machen, ein Parallelitätsproblem zu reproduzieren, da nicht alle Threads parallel ausgeführt werden können.

Um dies zu umgehen, lassen Sie dieCountdownLatch anders funktionieren als im vorherigen Beispiel. Anstatt einen übergeordneten Thread zu blockieren, bis einige untergeordnete Threads beendet sind, können wir jeden untergeordneten Thread blockieren, bis alle anderen gestartet sind.

Ändern Sie die Methoderun()o, dass sie vor der Verarbeitung blockiert wird:

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();
        }
    }
}

Ändern wir nun unseren Test so, dass er blockiert, bis alleWorkers gestartet wurden, dieWorkers, entsperrt und dann blockiert, bis dieWorkers beendet sind:

@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"
      );
}

Dieses Muster ist sehr nützlich, um Parallelitätsfehler zu reproduzieren. Es kann verwendet werden, um Tausende von Threads zu zwingen, einige Logik parallel auszuführen.

5. CountdownLatch vorzeitig beenden

Manchmal kann es vorkommen, dass dieWorkers fehlerhaft enden, bevor dieCountDownLatch. heruntergezählt werden. Dies kann dazu führen, dass sie niemals Null erreichen undawait() niemals enden:

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

Ändern wir unseren früheren Test so, dass einBrokenWorker, verwendet wird, um zu zeigen, wieawait() für immer blockiert:

@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();
}

Dies ist eindeutig nicht das von uns gewünschte Verhalten - es wäre viel besser, wenn die Anwendung fortgesetzt würde, als sie unendlich zu blockieren.

Um dies zu umgehen, fügen wir unserem Aufruf vonawait(). ein Timeout-Argument hinzu

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

Wie wir sehen können, wird der Test irgendwann eine Zeitüberschreitung aufweisen undawait() wirdfalse zurückgeben.

6. Fazit

In dieser Kurzanleitung haben wir gezeigt, wie wirCountDownLatch verwenden können, um einen Thread zu blockieren, bis andere Threads die Verarbeitung abgeschlossen haben.

Wir haben auch gezeigt, wie es zum Debuggen von Parallelitätsproblemen verwendet werden kann, indem sichergestellt wird, dass Threads parallel ausgeführt werden.

Die Implementierung dieser Beispiele kannover on GitHub gefunden werden; Dies ist ein Maven-basiertes Projekt und sollte daher so wie es ist einfach auszuführen sein.