CountDownLatch.await(): примеры (JAVA)

Работа CountDownLatch.await() в Java
Раздел: Многопоточность, Синхронизаторы
CountDownLatch.await(): void

Описание метода await()

Класс java.util.concurrent.CountDownLatch предоставляет механизм ожидания, основанный на счётчике. Метод await() блокирует вызывающий поток до тех пор, пока внутренний счётчик не достигнет нуля, либо пока не произойдёт прерывание.

Доступные сигнатуры:

  • public void await() throws InterruptedException - блокировка до достижения нуля или до прерывания; не возвращает значения.
  • public boolean await(long timeout, java.util.concurrent.TimeUnit unit) throws InterruptedException - блокировка максимум указанное время; возвращает true, если счётчик достиг нуля до истечения времени, иначе false. Также может быть прервано с генерированием InterruptedException.

Аргументы и возвращаемые значения:

  • Аргумент timeout - числовое значение лимита ожидания.
  • Аргумент unit - перечисление TimeUnit (NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS и т.д.). Передача null приведёт к NullPointerException.
  • Возвращаемое значение у тайм-аута - логическое: true при достижении нуля до таймаута, false при превышении времени; версия без таймаута возвращает ничего (void) и завершает работу только при достижении нуля или прерывании.

Исключения и гарантии видимости:

  • Оба метода бросают InterruptedException, если поток был прерван во время ожидания.
  • Каждый вызов countDown() happens-before завершению любого успешного вызова await(). Это обеспечивает видимость изменений, выполненных до countDown(), в потоках, продолживших работу после await().

Особенности поведения:

  • CountDownLatch не является цикличным: после достижения нуля его нельзя «сбросить» и повторно использовать. Для повторного использования подходят другие примитивы, например CyclicBarrier или Phaser.
  • Несколько потоков могут одновременно вызывать await(); все они разблокируются, когда счётчик достигает нуля.
  • Если счётчик уже равен нулю, вызов await() возвращает немедленно.

Короткие примеры использования

Пример 1: простой сценарий, где главный поток ждёт завершения двух рабочих потоков.

import java.util.concurrent.CountDownLatch;

public class Example1 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);

        new Thread(() -> {
            try { Thread.sleep(300); } catch (InterruptedException e) { }
            System.out.println("worker1 done");
            latch.countDown();
        }).start();

        new Thread(() -> {
            try { Thread.sleep(200); } catch (InterruptedException e) { }
            System.out.println("worker2 done");
            latch.countDown();
        }).start();

        System.out.println("main waiting");
        latch.await();
        System.out.println("main continues");
    }
}
main waiting
worker2 done
worker1 done
main continues

Пример 2: версия с таймаутом, где ожидание истекает и возвращает false.

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Example2 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);

        System.out.println(latch.await(100, TimeUnit.MILLISECONDS)); // нет decrement, таймаут
    }
}
false

Пример 3: прерывание ожидания вызывает InterruptedException.

import java.util.concurrent.CountDownLatch;

public class Example3 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);

        Thread t = new Thread(() -> {
            try {
                latch.await();
                System.out.println("unblocked");
            } catch (InterruptedException e) {
                System.out.println("was interrupted");
            }
        });

        t.start();
        Thread.sleep(100);
        t.interrupt();
    }
}
was interrupted

Аналоги внутри Java и их особенности

  • CyclicBarrier - барьер для группы потоков, используется, когда требуется многократно синхронизировать набор потоков; возвращает участников на каждом цикле и повторно применим.
  • Phaser - более гибкая и расширяемая синхронизация фаз; поддерживает динамическое добавление и удаление участников и многофазные схемы.
  • Semaphore - ограничение количества одновременных ресурсов; не прямой аналог, но иногда применяется для ожидания освобождения ресурсов.
  • CompletableFuture / Future - удобнее для асинхронных вычислений и композиции задач, особенно при работе с результатами; await-паттерн заменяется вызовами get() или комбинациями методов CompletableFuture.
  • Thread.join() - простая альтернатива, если требуется дождаться конкретного потока; не подходит для ожидания множества анонимных задач в пуле без явных ссылок на потоки.

Выбор зависит от сценария: для одноразового ожидания нескольких событий подходит CountDownLatch; для многоразовой синхронизации - CyclicBarrier или Phaser; для работы с результатами и композицией удобнее CompletableFuture.

Аналоги в других языках и основные отличия

Краткие эквиваленты и подходы в популярных языках:

  • Go: sync.WaitGroup - наиболее близкий аналог. Пример:
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        time.Sleep(200 * time.Millisecond)
        fmt.Println("worker1 done")
        wg.Done()
    }()

    go func() {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("worker2 done")
        wg.Done()
    }()

    fmt.Println("main waiting")
    wg.Wait()
    fmt.Println("main continues")
}
main waiting
worker2 done
worker1 done
main continues
  • C#: System.Threading.CountdownEvent - функционально близок. Пример:
using System;
using System.Threading;

class Program {
    static void Main() {
        var latch = new CountdownEvent(2);

        new Thread(() => { Thread.Sleep(200); Console.WriteLine("w1"); latch.Signal(); }).Start();
        new Thread(() => { Thread.Sleep(100); Console.WriteLine("w2"); latch.Signal(); }).Start();

        latch.Wait();
        Console.WriteLine("main continues");
    }
}
w2
w1
main continues
  • Python: нет точного аналога в стандартной библиотеке, но есть threading.Barrier и threading.Event. Для ожидания завершения множества потоков часто применяется набор объектов Thread + join или concurrent.futures:
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=2) as ex:
    futures = [ex.submit(lambda t: (print(f'w{i} done'), time.sleep(0.1))[0], i) for i in range(2)]
    for f in futures:
        f.result()
    print('main continues')
w0 done
w1 done
main continues
  • JavaScript (Node.js): паттерн с Promise.all выполняет роль ожидания множества асинхронных операций:
Promise.all([
  new Promise(r => setTimeout(() => { console.log('w1'); r(); }, 200)),
  new Promise(r => setTimeout(() => { console.log('w2'); r(); }, 100))
]).then(() => console.log('main continues'));
w2
w1
main continues
  • Kotlin: используется тот же java.util.concurrent.CountDownLatch или корутины с join и Deferred.
  • PHP, SQL, Lua: прямых встроенных аналогов нет. Применяются внешние расширения (Swoole, pthreads), очереди сообщений или управляющие механизмы на уровне процессов. Отличие: в большинстве сценариев синхронизация в этих языках реализуется на уровне событий или потоков ОС, а не стандартной библиотеки в одном стиле.

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

Типичные ошибки при использовании await()

  • Забывание вызовов countDown(), приводящее к вечному ожиданию. Пример:
// Пример: main будет вечно ждать, так как countDown не вызывается
CountDownLatch latch = new CountDownLatch(1);
System.out.println("about to wait");
latch.await(); // блокировка навсегда
// (приложение зависает, никакого вывода после "about to wait")
  • Игнорирование InterruptedException. Если исключение не обрабатывается, возможны некорректные состояния и потеря информации о прерывании. Пример неправильной обработки:
try {
    latch.await();
} catch (InterruptedException ignored) {
    // пустой catch - потеря сигнала прерывания
}
// программа игнорирует прерывание и продолжает без реакций
  • Передача null в аргумент TimeUnit в версии с таймаутом приводит к NullPointerException. Пример:
latch.await(100, null);
Exception in thread "main" java.lang.NullPointerException
    at java.base/.../CountDownLatch.await(CountDownLatch.java:...)
  • Использование CountDownLatch как многоразового барьера (ожидание-повтор), хотя он одноразовый. Частая ошибка - попытка переиспользовать тот же экземпляр после достижения нуля; повторные ожидания возвращают немедленно, но повторного увеличения счётчика не предусмотрено.

Изменения в методе за последние версии Java

CountDownLatch и его методы await() присутствуют в Java с версии 5 (JDK 1.5). За последующие релизы изменений сигнатур этих методов не происходило. Начиная с более новых версий платформы появились альтернативы в пакете java.util.concurrent (например, Phaser) и улучшения в производительности библиотеки в целом, но сам API CountDownLatch остался стабильным.

Расширенные и нетипичные примеры

Пример A: gate для одновременного старта рабочих потоков (start gate pattern).

Пример java
import java.util.concurrent.CountDownLatch;

public class StartGate {
    public static void main(String[] args) throws InterruptedException {
        int n = 3;
        CountDownLatch startGate = new CountDownLatch(1);
        CountDownLatch endGate = new CountDownLatch(n);

        for (int i = 0; i < n; i++) {
            final int id = i;
            new Thread(() -> {
                try {
                    startGate.await(); // ждём общего старта
                    System.out.println("worker " + id + " started");
                    Thread.sleep(100 + id * 50);
                    System.out.println("worker " + id + " done");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    endGate.countDown();
                }
            }).start();
        }

        System.out.println("releasing start gate");
        startGate.countDown(); // все рабочие стартуют почти одновременно
        endGate.await();
        System.out.println("all done");
    }
}
releasing start gate
worker 0 started
worker 1 started
worker 2 started
worker 0 done
worker 1 done
worker 2 done
all done

Комментарий: шаблон полезен для измерений и для синхронного старта задач.

Пример B: ожидание с таймаутом и fallback-логика.

Пример java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class TimedFallback {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);

        new Thread(() -> {
            try {
                Thread.sleep(500); // медленная инициализация
                System.out.println("init done");
                latch.countDown();
            } catch (InterruptedException e) { }
        }).start();

        if (!latch.await(200, TimeUnit.MILLISECONDS)) {
            System.out.println("fallback used");
        } else {
            System.out.println("proceed normally");
        }
    }
}
fallback used
init done

Комментарий: полезно при ожидании холодного старта сервиса с ограничением времени.

Пример C: использование вместе с ExecutorService и обработка прерываний.

Пример java
import java.util.concurrent.*;

public class ExecutorLatch {
    public static void main(String[] args) throws InterruptedException {
        int tasks = 5;
        CountDownLatch latch = new CountDownLatch(tasks);
        ExecutorService ex = Executors.newFixedThreadPool(3);

        for (int i = 0; i < tasks; i++) {
            ex.submit(() -> {
                try {
                    // работа
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }

        // ожидание со временем и аккуратная остановка пула
        if (!latch.await(1, TimeUnit.SECONDS)) {
            System.out.println("timeout, shutdown now");
            ex.shutdownNow();
        } else {
            System.out.println("all tasks finished");
            ex.shutdown();
        }
    }
}
all tasks finished

Комментарий: сочетание таймаутов и корректного завершения пула даёт управляемую обработку долгих задач.

Пример D: использование нескольких лочек для построения конвейера (pipeline).

Пример java
import java.util.concurrent.CountDownLatch;

public class Pipeline {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch stage1 = new CountDownLatch(1);
        CountDownLatch stage2 = new CountDownLatch(1);

        new Thread(() -> {
            try {
                // этап 1
                System.out.println("stage1 work");
                Thread.sleep(100);
                stage1.countDown();
            } catch (InterruptedException e) {}
        }).start();

        new Thread(() -> {
            try {
                stage1.await();
                System.out.println("stage2 work");
                Thread.sleep(100);
                stage2.countDown();
            } catch (InterruptedException e) {}
        }).start();

        stage2.await();
        System.out.println("pipeline finished");
    }
}
stage1 work
stage2 work
pipeline finished

Комментарий: несколько latch позволяют описать последовательные стадии без явных очередей.

Пример E: использование для тестирования асинхронного кода (ожидание события из теста).

Пример java
// В тесте: создаётся CountDownLatch(1), запускается асинхронная операция, в колбэке вызывается countDown(),
// тест ждёт с разумным таймаутом и проверяет результат.
// Ожидаемый результат теста: успешное завершение до таймаута либо явный провал с сообщением о таймауте

джава CountDownLatch.await() function comments

En
CountDownLatch.await() Блокирует поток до обнуления счетчика