CountDownLatch в Java с примерами
При разработке конкурентных Java-приложений часто необходимо синхронизировать несколько потоков. Класс CountDownLatch помогает организовать такую координацию: один или несколько потоков могут ждать, пока другие потоки завершат заданный набор операций.
Что такое CountDownLatch?
Представьте подготовку к забегу. Все участники должны стартовать одновременно, поэтому они ждут окончания обратного отсчёта. Когда счётчик достигает нуля, забег начинается. CountDownLatch работает похожим образом: при создании он получает число событий или задач, которые должны произойти, прежде чем ожидающие потоки смогут продолжить работу.
Latch является одноразовым. После достижения нуля его счётчик нельзя увеличить или вернуть к исходному значению. Если нужен многократно используемый барьер для одной группы потоков, лучше рассмотреть CyclicBarrier.
Как работает CountDownLatch?
- Инициализация. Объект создаётся с начальным count — количеством вызовов
countDown(), необходимых для открытия latch. - Уменьшение счётчика. Каждый
countDown()уменьшает значение на единицу. Дополнительные вызовы после нуля ничего не меняют. - Ожидание.
await()блокирует вызывающий поток, пока счётчик не станет равен нулю. - Гарантия видимости. Действия до
countDown()становятся видимыми потоку, который успешно вернулся изawait().
Базовый пример
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
private static final int NUMBER_OF_TASKS = 3;
private static final CountDownLatch latch = new CountDownLatch(NUMBER_OF_TASKS);
public static void main(String[] args) {
for (int i = 0; i < NUMBER_OF_TASKS; i++) {
new Thread(new Task()).start();
}
try {
latch.await(); // Главный поток ждёт нулевого счётчика
System.out.println("All tasks are completed. Main thread resumes.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
static class Task implements Runnable {
@Override
public void run() {
try {
// Имитация работы
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " completed.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // Уменьшаем счётчик в любом случае
}
}
}
}
В этом примере CountDownLatch получает начальное значение 3. Запускаются три задачи, каждая имитирует работу в течение секунды и затем вызывает countDown(). Главный поток останавливается на latch.await() и возобновляется только после завершения всех задач.
Вызов находится в finally, поэтому ошибка отдельной задачи не оставит главный поток в вечном ожидании. Такое решение означает «задача закончила попытку», а не обязательно «задача завершилась успешно»; реальный код должен отдельно сохранять ошибки или результаты.
Пример с тайм-аутом
Иногда ждать бесконечно нельзя. Перегруженный await() возвращает boolean и позволяет ограничить ожидание.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchTimeoutExample {
private static final int NUMBER_OF_TASKS = 3;
private static final CountDownLatch latch = new CountDownLatch(NUMBER_OF_TASKS);
public static void main(String[] args) {
for (int i = 0; i < NUMBER_OF_TASKS; i++) {
new Thread(new Task()).start();
}
try {
boolean completed = latch.await(5, TimeUnit.SECONDS);
if (completed) {
System.out.println("All tasks are completed. Main thread resumes.");
} else {
System.out.println("Timeout occurred before all tasks completed.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
static class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " completed.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
}
}
}
Главный поток ждёт не более пяти секунд. Если за это время все три задачи уменьшили счётчик, await() возвращает true. В противном случае он возвращает false, и приложение может отменить незавершённые операции, сообщить об ошибке или перейти к резервному сценарию. Сам тайм-аут не останавливает worker threads автоматически.
Практические сценарии CountDownLatch
Одновременный запуск нескольких потоков. Latch со значением 1 удерживает работников до общего стартового сигнала:
import java.util.concurrent.CountDownLatch; public class ConcurrentStartExample { private static final int NUMBER_OF_THREADS = 5; private static final CountDownLatch startLatch = new CountDownLatch(1); public static void main(String[] args) { for (int i = 0; i < NUMBER_OF_THREADS; i++) { new Thread(new Worker()).start(); } try { System.out.println("Ready... Set..."); Thread.sleep(1000); // Подготовительная работа startLatch.countDown(); System.out.println("Go!"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } static class Worker implements Runnable { @Override public void run() { try { startLatch.await(); System.out.println(Thread.currentThread().getName() + " is working."); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }Все пять работников запускаются и ждут. Единственный
countDown()открывает latch и позволяет им продолжить почти одновременно. Этот приём удобен в нагрузочных тестах или при запуске группы задач после завершения инициализации.Ожидание завершения нескольких потоков. Latch со значением, равным числу задач, позволяет агрегировать их результаты:
import java.util.concurrent.CountDownLatch; public class WaitForCompletionExample { private static final int NUMBER_OF_TASKS = 3; private static final CountDownLatch completionLatch = new CountDownLatch(NUMBER_OF_TASKS); public static void main(String[] args) { for (int i = 0; i < NUMBER_OF_TASKS; i++) { new Thread(new SubTask()).start(); } try { completionLatch.await(); System.out.println("All subtasks completed. Aggregating results."); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } static class SubTask implements Runnable { @Override public void run() { try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + " subtask completed."); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { completionLatch.countDown(); } } } }Главный поток приступает к агрегации только после того, как все subtasks завершили попытку выполнения. В современном коде похожую задачу также решают
ExecutorService,FutureилиCompletableFuture, ноCountDownLatchостаётся простым способом выразить зависимость от фиксированного числа событий.
Итоги
CountDownLatch — надёжный одноразовый механизм координации потоков. Он подходит для общего старта, ожидания завершения набора задач и разделения большой работы на независимые части. Важно правильно выбрать начальное значение, гарантировать countDown() для каждого ожидаемого события и обрабатывать interruption. Если ожидание не должно быть бесконечным, используйте тайм-аут и явно определите поведение незавершённых задач.