Руководство по Java Phaser

1. Обзор

В этой статье мы рассмотрим Phaser конструкцию из пакета java.util.concurrent , Эта конструкция очень похожа на ссылку:/java-countdown-latch[ CountDownLatch ], которая позволяет нам координировать выполнение потоков. По сравнению с CountDownLatch , он имеет некоторые дополнительные функции.

Phaser - это барьер, на котором динамическое число потоков должно ждать, прежде чем продолжить выполнение. В CountDownLatch этот номер не может быть настроен динамически и должен быть указан при создании экземпляра.

2. Phaser API

Phaser позволяет нам строить логику, в которой потоки должны ждать на барьере, прежде чем перейти к следующему шагу выполнения .

Мы можем координировать несколько этапов выполнения, повторно используя экземпляр Phaser для каждого этапа программы. Каждая фаза может иметь различное количество потоков, ожидающих перехода к другой фазе. Мы рассмотрим пример использования фаз позже.

Чтобы участвовать в координации, поток должен сам register () с экземпляром Phaser . Обратите внимание, что это только увеличивает количество зарегистрированных сторон, и мы не можем проверить, зарегистрирован ли текущий поток, - нам пришлось бы создать подкласс для реализации, поддерживающей это.

Поток сообщает, что достиг барьера, вызывая arriveAndAwaitAdvance () , который является методом блокировки. Когда количество прибывших участников будет равно количеству зарегистрированных участников, выполнение программы будет продолжено , а число этапов увеличится. Мы можем получить текущий номер фазы, вызвав метод getPhase () .

Когда поток завершает свою работу, мы должны вызвать метод arriveAndDeregister () , чтобы указать, что текущий поток больше не должен учитываться в этой конкретной фазе.

3. Реализация логики с использованием API Phaser

Допустим, мы хотим скоординировать несколько этапов действий. Три потока будут обрабатывать первый этап, а два потока - второй этап.

Мы создадим класс LongRunningAction , который реализует интерфейс Runnable :

class LongRunningAction implements Runnable {
    private String threadName;
    private Phaser ph;

    LongRunningAction(String threadName, Phaser ph) {
        this.threadName = threadName;
        this.ph = ph;
        ph.register();
    }

    @Override
    public void run() {
        ph.arriveAndAwaitAdvance();
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ph.arriveAndDeregister();
    }
}

Когда создается наш класс действий, мы регистрируемся в экземпляре Phaser с помощью метода register () . Это увеличит количество потоков, используя этот конкретный Phaser.

Вызов arriveAndAwaitAdvance () заставит текущий поток ждать на барьере. Как уже упоминалось, когда количество прибывших сторон становится таким же, как количество зарегистрированных сторон, исполнение будет продолжено.

После завершения обработки текущий поток отменяет свою регистрацию, вызывая метод arriveAndDeregister () .

Давайте создадим контрольный пример, в котором мы запустим три потока LongRunningAction и заблокируем барьер. Далее, после того, как действие закончено, мы создадим два дополнительных потока LongRunningAction , которые будут выполнять обработку следующего этапа.

При создании экземпляра Phaser из основного потока мы передаем 1 в качестве аргумента. Это эквивалентно вызову метода register () из текущего потока. Мы делаем это, потому что, когда мы создаем три рабочих потока, основной поток является координатором, и поэтому Phaser должен иметь четыре зарегистрированных потока:

ExecutorService executorService = Executors.newCachedThreadPool();
Phaser ph = new Phaser(1);

assertEquals(0, ph.getPhase());

Фаза после инициализации равна нулю.

Класс Phaser имеет конструктор, в котором мы можем передать ему родительский экземпляр. Это полезно в тех случаях, когда у нас большое количество сторон, которые могут столкнуться с большими расходами на синхронизацию.

В таких ситуациях экземпляры Phasers могут быть настроены так, чтобы группы подфазеров имели общего родителя.

Далее, давайте запустим три потока действий LongRunningAction , которые будут ожидать на барьере, пока мы не вызовем метод arriveAndAwaitAdvance () из основного потока.

Помните, что мы инициализировали наш Phaser с помощью 1 и вызвали register () еще три раза. Теперь три потока действий объявили, что достигли барьера, поэтому необходим еще один вызов arriveAndAwaitAdvance () - один из основного потока:

executorService.submit(new LongRunningAction("thread-1", ph));
executorService.submit(new LongRunningAction("thread-2", ph));
executorService.submit(new LongRunningAction("thread-3", ph));

ph.arriveAndAwaitAdvance();

assertEquals(1, ph.getPhase());

После завершения этой фазы метод getPhase () вернет единицу, так как программа завершила обработку первого шага выполнения.

Допустим, два потока должны провести следующий этап обработки.

Мы можем использовать Phaser для достижения этой цели, потому что это позволяет нам динамически настраивать количество потоков, которые должны ждать на барьере. Мы запускаем два новых потока, но они не будут выполняться до тех пор, пока не будет вызван вызов arriveAndAwaitAdvance () из основного потока (такой же, как в предыдущем случае):

executorService.submit(new LongRunningAction("thread-4", ph));
executorService.submit(new LongRunningAction("thread-5", ph));
ph.arriveAndAwaitAdvance();

assertEquals(2, ph.getPhase());

ph.arriveAndDeregister();

После этого метод getPhase () вернет номер фазы, равный двум. Когда мы хотим завершить нашу программу, нам нужно вызвать метод arriveAndDeregister () , поскольку основной поток все еще зарегистрирован в Phaser. Когда в результате отмены регистрации число зарегистрированных сторон становится равным нулю, Phaser становится terminated. All вызовы методов синхронизации больше не будут блокироваться и будут возвращаться немедленно.

Запуск программы приведет к следующему выводу (полный исходный код с инструкциями строки печати можно найти в репозитории кода):

This is phase 0
This is phase 0
This is phase 0
Thread thread-2 before long running action
Thread thread-1 before long running action
Thread thread-3 before long running action
This is phase 1
This is phase 1
Thread thread-4 before long running action
Thread thread-5 before long running action

Мы видим, что все потоки ожидают выполнения, пока не откроется барьер. Следующий этап выполнения выполняется только после успешного завершения предыдущего.

4. Заключение

В этом руководстве мы взглянули на конструкцию Phaser из java.util.concurrent и реализовали координационную логику с несколькими фазами, используя класс Phaser .

Реализация всех этих примеров и фрагментов кода может быть найдена в GitHub проекте - это проект Maven, поэтому он должен быть легко импортировать и запускать как есть.