Введение
В многопоточном программировании часто возникает необходимость синхронизировать выполнение нескольких задач: одна или несколько операций не должны начинаться, пока не завершатся другие. Для этого в Java существует паттерн барьерной синхронизации (Barrier Synchronization), который позволяет потокам «встретиться» в определённой точке и продолжить выполнение только после того, как все достигнут этой точки.
В Java в пакете java.util.concurrent для реализации этого паттерна доступны три мощных класса:
- CountDownLatch
- CyclicBarrier
- Phaser
Давайте разберём каждый из них подробно, с примерами и объяснениями.
Участники барьерной синхронизации
В паттерне барьерной синхронизации выделяют два типа участников:
1️⃣ Исполнители (Workers)
Это потоки, которые выполняют параллельные задачи и сигнализируют о завершении своей работы. Например, загрузка данных, обработка изображений, вычисления.
2️⃣ Наблюдатели (Observers / Waiters)
Это потоки, которые ждут завершения определённого количества задач, прежде чем продолжить своё выполнение. Например, главный поток, который должен собрать результаты всех вычислений.
1. CountDownLatch — Одноразовый счётчик-барьер
CountDownLatch — это простой и эффективный механизм, который позволяет одному или нескольким потокам ждать, пока другие потоки не завершат выполнение определённого количества операций.
🔧 Как работает:
- Создаётся с начальным счётчиком: new CountDownLatch(n)
- Каждый исполнитель вызывает countDown() по завершении своей задачи → счётчик уменьшается на 1.
- Наблюдатель вызывает await() — блокируется, пока счётчик не достигнет нуля.
- Одноразовый: после достижения нуля счётчик нельзя сбросить.
📌 Пример: Запуск сервисов перед стартом приложения
💡 Когда использовать:
- Инициализация перед стартом основной логики.
- Ожидание завершения фоновых задач.
- Тестирование многопоточных сценариев.
2. CyclicBarrier — Многоразовый барьер
CyclicBarrier позволяет группе потоков многократно ждать друг друга в одной точке. В отличие от CountDownLatch, его можно переиспользовать после срабатывания.
🔧 Как работает:
- Создаётся с количеством участников: new CyclicBarrier(n)
- Каждый поток вызывает barrier.await() — блокируется, пока все участники не вызовут await().
- После того как все дойдут до барьера — все потоки одновременно продолжают выполнение.
- Можно задать действие при срабатывании барьера (Runnable), которое выполнится в одном из потоков перед разблокировкой.
📌 Пример: Расчёт в несколько этапов
💡 Когда использовать:
- Многопроходные вычисления
- Пошаговая синхронизация потоков.
- Игровые циклы, где все игроки должны закончить ход, прежде чем начнётся следующий.
3. Phaser — Гибкий и динамический барьер
Phaser — это самый гибкий и мощный из трёх механизмов. Он объединяет возможности CountDownLatch и CyclicBarrier, но добавляет динамическое управление участниками, фазами и возможностью завершения.
🔧 Как работает:
- Участники могут регистрироваться и сниматься динамически (register(), arriveAndDeregister()).
- Поддерживает множество фаз (phases), которые автоматически увеличиваются.
- Можно проверять текущую фазу, количество зарегистрированных/прибывших участников.
- Поддерживает иерархию (дерево фазеров) для масштабирования.
📌 Пример: Динамическая обработка задач с возможностью добавления новых
💡 Когда использовать:
- Динамически изменяющееся количество задач.
- Сложные многофазные алгоритмы.
- Когда нужно гибко управлять жизненным циклом участников.
- Большие распределённые вычисления с возможностью адаптации.
Советы по выбору
- CountDownLatch — если вам нужно один раз дождаться завершения фиксированного числа задач.
- CyclicBarrier — если потоки должны многократно синхронизироваться в одной точке (например, итерации).
- Phaser — если вам нужна максимальная гибкость, динамическое управление участниками и многофазовые вычисления.
Заключение
Барьерная синхронизация — мощный инструмент для координации потоков в Java. Понимание различий между CountDownLatch, CyclicBarrier и Phaser позволяет писать более эффективный, читаемый и надёжный многопоточный код.
Не используйте synchronized и wait()/notify() там, где можно применить высокоуровневые абстракции из java.util.concurrent. Они не только проще в использовании, но и менее подвержены ошибкам.
Рассмотренные в статье примеры, вы можете найти по адресу: