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

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

Общее описание метода

Метод CountDownLatch.countDown() из пакета java.util.concurrent уменьшает внутренний счетчик экземпляра CountDownLatch на единицу. Первоначальный счетчик задается в конструкторе new CountDownLatch(count). Когда счетчик достигает нуля, все потоки, ожидающие вызова await() на этом же объекте, пробуждаются.

Ключевые свойства:

  • Аргументы: отсутствуют.
  • Возвращаемое значение: void.
  • Идемпотентность при достижении нуля: дополнительные вызовы countDown() после достижения нуля не приводят к отрицательному значению счетчика и не бросают исключений.
  • Блокирующее поведение: сам countDown() не блокирует вызывающий поток. Блокировка реализуется методами await() и await(long, TimeUnit).
  • Потокобезопасность: метод реализован для конкурентного использования несколькими потоками.
  • Гарантии видимости: когда счетчик переходит в ноль, происходит эффект happens-before для операций до countDown() и операций после успешного await().

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

Простые варианты использования

Ниже приведены короткие примеры. В каждом примере показан код и возможный вывод.

1) Базовый пример: главный поток ждет завершения двух воркеров.

import java.util.concurrent.CountDownLatch;

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

        new Thread(() -> {
            System.out.println("Worker A started");
            // имитация работы
            try { Thread.sleep(200); } catch (InterruptedException ignored) {}
            latch.countDown();
            System.out.println("Worker A finished");
        }).start();

        new Thread(() -> {
            System.out.println("Worker B started");
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
            latch.countDown();
            System.out.println("Worker B finished");
        }).start();

        latch.await();
        System.out.println("All workers completed");
    }
}
Возможный вывод:
Worker A started
Worker B started
Worker B finished
Worker A finished
All workers completed

2) Вызов countDown больше раз, чем исходный счетчик (без ошибок).

import java.util.concurrent.CountDownLatch;

public class ExtraCountDown {
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(1);
        latch.countDown(); // счетчик становится 0
        latch.countDown(); // дополнительных ошибок не будет
        System.out.println("After extra countDown");
    }
}
Вывод:
After extra countDown

3) await с таймаутом показывает, что countDown может не успеть.

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

public class TimeoutExample {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        // только один воркер
        new Thread(() -> {
            try { Thread.sleep(300); } catch (InterruptedException ignored) {}
            latch.countDown();
        }).start();

        boolean finished = latch.await(200, TimeUnit.MILLISECONDS);
        System.out.println("Await finished: " + finished);
    }
}
Вывод:
Await finished: false

Аналоги в Java и их отличия

  • Phaser: более гибкий инструмент для многоступенчатой синхронизации и повторного использования. Предпочтительнее, когда требуется несколько фаз или динамическое изменение числа участников.
  • CyclicBarrier: синхронизирует набор потоков на одной фазе и автоматически сбрасывается для повторного использования. Подходит, когда участники должны синхронизироваться много раз.
  • Semaphore: управляет количеством одновременных доступов, а не ожиданием завершения фиксированного числа задач. Используется для ограничения параллелизма.
  • CompletableFuture: предоставляет декларативный стиль композиции асинхронных задач, удобен для независимых задач с обработкой результата и цепочками обработки.
  • CountDownLatch предпочтительно, когда требуется простое одноразовое ожидание завершения заранее известного числа событий.

Соответствия в других языках

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

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

import (
    "fmt"
    "sync"
)

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

    go func() {
        defer wg.Done()
        fmt.Println("Worker 1 done")
    }()

    go func() {
        defer wg.Done()
        fmt.Println("Worker 2 done")
    }()

    wg.Wait()
    fmt.Println("All completed")
}
Возможный вывод:
Worker 1 done
Worker 2 done
All completed
  • C#: System.Threading.CountdownEvent - поведение очень похоже.
using System;
using System.Threading;

class Program {
  static void Main() {
    var latch = new CountdownEvent(2);
    new Thread(() => { Thread.Sleep(100); latch.Signal(); Console.WriteLine("A"); }).Start();
    new Thread(() => { Thread.Sleep(200); latch.Signal(); Console.WriteLine("B"); }).Start();
    latch.Wait();
    Console.WriteLine("Done");
  }
}
Возможный вывод:
A
B
Done
  • Python: в стандартной библиотеке прямого аналога нет, используются threading.Barrier, threading.Event или сочетание счетчика с блокировкой. В asyncio используется asyncio.gather для ожидания набора задач.
# Пример с threading.Event как упрощенный сигнал
import threading

latch_event = threading.Event()

def worker():
    print('Worker started')
    latch_event.set()  # не эквивалент точный, показан сигнал

threading.Thread(target=worker).start()
# ожидание сигнала
latch_event.wait()
print('Notified')
Возможный вывод:
Worker started
Notified
  • JavaScript: вместо блокировок часто используются Promise.all для ожидания множества асинхронных операций.
// Node.js / браузер
Promise.all([
  new Promise(res => setTimeout(() => res('A'), 100)),
  new Promise(res => setTimeout(() => res('B'), 200))
]).then(results => console.log('All:', results));
Вывод через ~200ms:
All: [ 'A', 'B' ]
  • PHP: нет встроенной структуры; в расширениях для параллелизма (pthreads, parallel) применяются другие примитивы, часто используется синхронизация через каналы или семафоры.
  • Kotlin: на JVM использует те же примитивы, но для корутин предпочитаются Deferred и awaitAll из kotlinx.coroutines.
  • Lua: в стандартной библиотеке не предусмотрено, в средах с потоками или корутинами применяются каналы или внешние библиотеки.

Отличие от Java: в некоторых языках модель асинхронности не блокирующая (Promise/async), поэтому ожидание завершения реализуется через композицию колбэков или обещаний, а не через блокирующие примитивы.

Типичные ошибки и их проявления

  • Неправильный начальный счетчик. Если указать слишком большое значение, ожидание никогда не закончится. Пример:
CountDownLatch latch = new CountDownLatch(3);
// Запущены только 2 задачи, забыта третья signal
// latch.await(); // будет блокировать навсегда
Результат: главный поток может блокироваться бесконечно.
  • Ожидание повторного использования. CountDownLatch одноразовая конструкция и не сбрасывается автоматически. Частая ошибка - ожидание, что latch можно переиспользовать для следующей фазы.
  • Вызов countDown() не в блоке finally. При выбросе исключения в задаче countDown может не выполниться и привести к зависанию ожидающих потоков. Рекомендуется размещать countDown() в finally:
try {
    // работа
} finally {
    latch.countDown();
}
Если убрать finally и случится исключение, ожидание может не завершиться.
  • Игнорирование InterruptedException у await(). При прерывании необходимо обрабатывать состояние потока и, при необходимости, корректно завершать работу.
  • Ожидание в UI-потоке. Блокировка основного потока интерфейса приведет к зависанию приложения.

История и изменения API

Класс CountDownLatch введен в Java 5 как часть пакета java.util.concurrent. Сам метод countDown() и семантика счётчика оставались стабильными в последующих версиях Java; существенных изменений в поведении или сигнатуре метода не было. При переходе к современным асинхронным подходам (CompletableFuture, реактивные библиотеки) появились альтернативные паттерны, но сам примитив сохраняет прежнюю роль для простых сценариев синхронизации.

Расширенные и редкие сценарии применения

Несколько более сложных примеров с пояснениями.

1) Использование как стартового шлюза (start gate) для одновременного старта множества воркеров.

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

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

        for (int i = 0; i < workers; i++) {
            final int id = i;
            new Thread(() -> {
                try {
                    startGate.await(); // ждет общего сигнала
                    System.out.println("Worker " + id + " running");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    endGate.countDown();
                }
            }).start();
        }

        // подготовка
        Thread.sleep(100);
        System.out.println("Releasing workers");
        startGate.countDown(); // все воркеры стартуют почти одновременно

        endGate.await();
        System.out.println("All workers done");
    }
}
Возможный вывод:
Releasing workers
Worker 0 running
Worker 1 running
Worker 2 running
All workers done

Комментарий: сочетание двух latch (startGate и endGate) дает контролируемый старт и надежный финиш.

2) Гарантия выполнения countDown в finally для избежания дедлоков и корректного учета завершений.

Пример java
// В фрагменте worker'а
try {
    // работа, может бросать исключения
} finally {
    latch.countDown(); // всегда уменьшается счетчик
}
Результат: даже при исключении главный поток не будет ждать навсегда.

3) Эмулирование повторного использования: создание нового CountDownLatch для каждой итерации или использование Phaser, если перезапуск после каждой фазы обязателен.

Пример java
// Псевдокод для цикла фаз
for (int phase = 0; phase < N; phase++) {
    CountDownLatch latch = new CountDownLatch(tasks);
    // запустить задачи, которые вызовут latch.countDown()
    latch.await();
}
Результат: каждая фаза использует новый latch и завершение каждой фазы корректно отслеживается.

4) Сбор частичных результатов с таймаутом: выявление сколько задач успели завершиться.

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

public class PartialResults {
    public static void main(String[] args) throws InterruptedException {
        int tasks = 5;
        CountDownLatch latch = new CountDownLatch(tasks);
        AtomicInteger completed = new AtomicInteger(0);
        ExecutorService es = Executors.newFixedThreadPool(tasks);

        for (int i = 0; i < tasks; i++) {
            es.submit(() -> {
                try {
                    // случайная длительность
                    Thread.sleep((long)(Math.random() * 400));
                    completed.incrementAndGet();
                } catch (InterruptedException ignored) {}
                finally {
                    latch.countDown();
                }
            });
        }

        boolean allDone = latch.await(250, TimeUnit.MILLISECONDS);
        System.out.println("Completed tasks: " + completed.get());
        System.out.println("All finished within timeout: " + allDone);
        es.shutdownNow();
    }
}
Возможный вывод:
Completed tasks: 3
All finished within timeout: false

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

5) Взаимодействие с CompletableFuture: запуск нескольких futures и ожидание их завершения без прямого использования CountDownLatch - демонстрирует альтернативный стиль.

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

public class FuturesExample {
    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newFixedThreadPool(2);
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "A", es);
        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "B", es);
        CompletableFuture.allOf(f1, f2).join();
        System.out.println("Results: " + f1.get() + ", " + f2.get());
        es.shutdown();
    }
}
Вывод:
Results: A, B

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

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

En
CountDownLatch.countDown() Уменьшает счетчик на единицу