Сравните CountDownLatch и CyclicBarrier для повторяющейся синхронизации нескольких фаз работы.
CountDownLatch предназначен для одноразового события: его счётчик уменьшается до нуля, после чего ожидающие потоки проходят, а сам latch нельзя сбросить. CyclicBarrier предназначен для повторяющихся фаз: заданное число потоков доходит до барьера, все одновременно переходят дальше, и тот же барьер можно использовать в следующем цикле.
Многопоточным программам часто требуется координировать группы потоков: дождаться готовности ресурсов, синхронно завершить фазу вычислений или начать работу после общего события. Низкоуровневые wait и notify позволяют это реализовать, но требуют вручную поддерживать состояния, обработку пробуждений и повторное использование координационного объекта.
CountDownLatch и CyclicBarrier предоставляют специализированные модели координации. Их разделение отражает две разные задачи: одноразовое открытие прохода и циклическое выравнивание потоков по фазам.
Если применить CountDownLatch для многофазного алгоритма, после достижения нулевого счётчика все последующие вызовы await() будут проходить сразу. Потоки перестанут ждать завершения текущей фазы, что может привести к чтению ещё не подготовленных данных.
Если применить CyclicBarrier вместо одноразового сигнала, потокам придётся участвовать в каждом достижении барьера. Для события вроде «конфигурация загружена» это усложняет модель и может привести к зависанию, если один из участников не должен повторно входить в барьер.
У CountDownLatch есть начальное значение счётчика. Каждый вызов countDown() уменьшает его, а await() блокирует поток, пока значение не станет нулём. Достижение нуля является одноразовым: увеличить счётчик или вернуть latch в исходное состояние нельзя.
У CyclicBarrier заранее задаётся число участников. Каждый участник вызывает await(), и только после прибытия всех участников они получают возможность продолжить. После этого барьер автоматически переходит в следующее поколение и может использоваться снова.
Минимальный пример различия:
Здесь start моделирует одноразовый сигнал старта, а phase синхронизирует завершение каждой из двух фаз. Для реальной запускаемой программы нужно передать один и тот же CyclicBarrier всем потокам и создать его с числом участников, равным числу потоков, которые обязаны доходить до каждого барьера.
У CyclicBarrier можно задать действие барьера. Оно выполняется одним из участников после прибытия последнего потока и до освобождения участников. Если участник прерван, истекает тайм-аут или действие барьера завершается ошибкой, барьер переходит в состояние broken; ожидающие и последующие участники получают исключение, пока барьер не будет сброшен или заменён.
У CountDownLatch тайм-аут или прерывание конкретного ожидающего потока не изменяют сам счётчик. У CyclicBarrier такие события влияют на состояние общего барьера, потому что нарушают согласованность текущей фазы между участниками.
В сервисе несколько рабочих потоков обрабатывают один пакет данных по этапам: загрузка, вычисление и агрегация. На каждом этапе поток должен дождаться остальных, иначе агрегация может начаться до завершения вычислений.
Вариант с отдельным CountDownLatch на каждую фазу работает, но требует создавать и передавать новый latch для каждого этапа. Это увеличивает количество состояний и риск ошибиться в выборе latch для конкретной фазы.
Вариант с одним CyclicBarrier естественно выражает повторяющуюся фазовую синхронизацию. Его недостаток — каждый этап должен завершаться всеми участниками; если рабочий поток может досрочно выйти, барьер нужно корректно сломать или перестроить, иначе остальные потоки будут ждать бесконечно.
Для такой задачи выбирают CyclicBarrier, задают тайм-аут ожидания и обрабатывают BrokenBarrierException. Для одноразового запуска после инициализации, напротив, выбирают CountDownLatch, потому что его невозможность сброса явно соответствует жизненному циклу события.
Что произойдёт, если один участник CyclicBarrier завершится до вызова await()?
Остальные участники не смогут достичь требуемого числа прибывших и будут ждать. Если не задан тайм-аут или не выполнена отмена барьера, это может привести к зависанию. Сам по себе CyclicBarrier не знает, что поток больше никогда не станет участником, поэтому жизненный цикл участников должен контролироваться приложением.
Можно ли использовать один CountDownLatch для ожидания готовности и для ожидания завершения работы?
Технически можно создать latch с подходящим счётчиком, но обычно это смешивает два разных события. Например, один счётчик может означать готовность потоков, а другой — завершение задач. Раздельные latch делают протокол понятнее и не позволяют завершению одной стадии случайно открыть ожидание другой.
Почему CyclicBarrier не заменяет Phaser во всех многофазных алгоритмах?
CyclicBarrier рассчитан на фиксированное число участников, которое задаётся при создании. Phaser поддерживает динамическую регистрацию и снятие регистрации участников, а также несколько фаз с отслеживанием номера текущей фазы. Поэтому CyclicBarrier проще и легче для стабильной группы потоков, а Phaser подходит для алгоритмов, где состав участников меняется во время выполнения.