Руководство по CountDownLatch в Java

Руководство по CountDownLatch в Java

1. Вступление

В этой статье мы дадим руководство по классуCountDownLatch и продемонстрируем, как его можно использовать на нескольких практических примерах.

По сути, используяCountDownLatch, мы можем заставить поток блокироваться до тех пор, пока другие потоки не завершат данную задачу.

2. Использование в параллельном программировании

Проще говоря,CountDownLatch имеет полеcounter, которое вы можете уменьшать по мере необходимости. Затем мы можем использовать его для блокировки вызывающего потока до тех пор, пока он не будет отсчитан до нуля.

Если бы мы выполняли некоторую параллельную обработку, мы могли бы создать экземплярCountDownLatch с тем же значением счетчика, что и количество потоков, с которыми мы хотим работать. Затем мы могли бы просто вызватьcountdown() после завершения каждого потока, гарантируя, что зависимый поток, вызывающийawait(), будет блокироваться до завершения рабочих потоков.

3. Ожидание завершения пула потоков

Давайте попробуем этот паттерн, создавWorker и используя полеCountDownLatch, чтобы сигнализировать о завершении:

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

Затем давайте создадим тест, чтобы доказать, что мы можем заставитьCountDownLatch ждать завершения экземпляровWorker:

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

Естественно, что «Защелка выпущена» всегда будет последним выводом, поскольку это зависит от освобожденияCountDownLatch.

Обратите внимание, что если бы мы не вызывалиawait(), мы не смогли бы гарантировать порядок выполнения потоков, поэтому тест случайно не прошел бы.

4. A Pool of Threads Waiting to Beginс

Если мы возьмем предыдущий пример, но на этот раз запустили тысячи потоков вместо пяти, вполне вероятно, что многие из предыдущих завершат обработку еще до того, как мы вызовемstart() для последующих. Это может затруднить попытку воспроизвести проблему параллелизма, поскольку мы не сможем заставить все наши потоки работать параллельно.

Чтобы обойти это, давайте заставимCountdownLatch работать иначе, чем в предыдущем примере. Вместо того, чтобы блокировать родительский поток до тех пор, пока некоторые дочерние потоки не закончатся, мы можем заблокировать каждый дочерний поток, пока все остальные не запустятся.

Давайте изменим наш методrun() так, чтобы он блокировался перед обработкой:

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

Теперь давайте изменим наш тест так, чтобы он блокировался до тех пор, пока не запустились всеWorkers, разблокируетWorkers,, а затем блокируется до тех пор, покаWorkers не завершится:

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

Этот шаблон действительно полезен при попытке воспроизвести ошибки параллелизма, так как он может быть использован для того, чтобы заставить тысячи потоков попытаться выполнить некоторую логику параллельно.

5. ЗавершениеCountdownLatch раньше

Иногда мы можем столкнуться с ситуацией, когдаWorkers завершается по ошибке перед обратным отсчетомCountDownLatch.. Это может привести к тому, что он никогда не достигнет нуля, аawait() никогда не завершится:

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

Давайте изменим наш предыдущий тест, чтобы использоватьBrokenWorker,, чтобы показать, какawait() будет блокироваться навсегда:

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

Понятно, что это не то поведение, которое нам нужно, - было бы гораздо лучше, если бы приложение продолжало работать, чем бесконечно блокировать.

Чтобы обойти это, давайте добавим аргумент тайм-аута к нашему вызовуawait().

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

Как мы видим, время ожидания теста истечет, иawait() вернетfalse.

6. Заключение

В этом кратком руководстве мы продемонстрировали, как можно использоватьCountDownLatch, чтобы заблокировать поток до тех пор, пока другие потоки не закончат некоторую обработку.

Мы также показали, как его можно использовать для устранения проблем с параллелизмом, обеспечивая параллельную работу потоков.

Реализацию этих примеров можно найтиover on GitHub; это проект на основе Maven, поэтому его легко запускать как есть.