Обзор java.util.concurrent

Обзор java.util.concurrent

1. обзор

Пакетjava.util.concurrent предоставляет инструменты для создания параллельных приложений.

В этой статье мы сделаем обзор всего пакета.

2. Основные компоненты

java.util.concurrent содержит слишком много функций, чтобы обсудить их в одной статье. В этой статье мы сосредоточимся в основном на некоторых наиболее полезных утилитах из этого пакета, таких как:

  • душеприказчик

  • ExecutorService

  • ScheduledExecutorService

  • Будущее

  • CountDownLatch

  • CyclicBarrier

  • семафор

  • ThreadFactory

  • BlockingQueue

  • DelayQueue

  • Замки

  • Phaser

Вы также можете найти много специальных статей для отдельных классов здесь.

2.1. Executorс

Executor - это интерфейс, представляющий объект, который выполняет предоставленные задачи.

Это зависит от конкретной реализации (откуда инициируется вызов), должна ли задача выполняться в новом или текущем потоке. Следовательно, используя этот интерфейс, мы можем отделить поток выполнения задачи от фактического механизма выполнения задачи.

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

Нам нужно создать invoker для создания экземпляра executor:

public class Invoker implements Executor {
    @Override
    public void execute(Runnable r) {
        r.run();
    }
}

Теперь мы можем использовать этот призыватель для выполнения задачи.

public void execute() {
    Executor executor = new Invoker();
    executor.execute( () -> {
        // task to be performed
    });
}

Здесь следует отметить, что если исполнитель не может принять задачу для выполнения, он выбрасываетRejectedExecutionException.

2.2. ExecutorServiceс

ExecutorService - полное решение для асинхронной обработки. Он управляет очередью в памяти и планирует отправленные задачи в зависимости от доступности потока.

Чтобы использоватьExecutorService,, нам нужно создать один классRunnable.

public class Task implements Runnable {
    @Override
    public void run() {
        // task details
    }
}

Теперь мы можем создать экземплярExecutorService и назначить эту задачу. Во время создания нам нужно указать размер пула потоков.

ExecutorService executor = Executors.newFixedThreadPool(10);

Если мы хотим создать однопоточный экземплярExecutorService, мы можем использоватьnewSingleThreadExecutor(ThreadFactory threadFactory) для создания экземпляра.

Как только исполнитель создан, мы можем использовать его для отправки задания.

public void execute() {
    executor.submit(new Task());
}

Мы также можем создать экземплярRunnable при отправке задачи.

executor.submit(() -> {
    new Task();
});

Это также идет с двумя из готовых методов завершения выполнения. Первый -shutdown(); он ожидает завершения выполнения всех отправленных задач. Другой метод -shutdownNow(), при которомh немедленно завершает все ожидающие / выполняющиеся задачи.

Существует также другой методawaitTermination(long timeout, TimeUnit unit), который принудительно блокируется до тех пор, пока все задачи не завершат выполнение после срабатывания события выключения или истечения времени ожидания выполнения, или пока сам поток выполнения не будет прерван

try {
    executor.awaitTermination( 20l, TimeUnit.NANOSECONDS );
} catch (InterruptedException e) {
    e.printStackTrace();
}

2.3. ScheduledExecutorServiceс

ScheduledExecutorService похож на интерфейсExecutorService,, но может периодически выполнять задачи.

Executor and ExecutorService‘s methods are scheduled on the spot without introducing any artificial delay. Ноль или любое отрицательное значение означает, что запрос должен быть выполнен немедленно.

Мы можем использовать интерфейсRunnable иCallable для определения задачи.

public void execute() {
    ScheduledExecutorService executorService
      = Executors.newSingleThreadScheduledExecutor();

    Future future = executorService.schedule(() -> {
        // ...
        return "Hello world";
    }, 1, TimeUnit.SECONDS);

    ScheduledFuture scheduledFuture = executorService.schedule(() -> {
        // ...
    }, 1, TimeUnit.SECONDS);

    executorService.shutdown();
}

ScheduledExecutorService также может запланировать задачуafter some given fixed delay:

executorService.scheduleAtFixedRate(() -> {
    // ...
}, 1, 10, TimeUnit.SECONDS);

executorService.scheduleWithFixedDelay(() -> {
    // ...
}, 1, 10, TimeUnit.SECONDS);

Здесь методscheduleAtFixedRate( Runnable command, long initialDelay, long period, TimeUnit unit ) создает и выполняет периодическое действие, которое вызывается сначала после заданной начальной задержки, а затем с заданным периодом до завершения работы экземпляра службы.

МетодscheduleWithFixedDelay( Runnable command, long initialDelay, long delay, TimeUnit unit ) создает и выполняет периодическое действие, которое вызывается сначала после заданной начальной задержки и повторно с заданной задержкой между завершением выполняемого действия и вызовом следующего.

2.4. Futureс

Future is used to represent the result of an asynchronous operation. В нем есть методы для проверки того, завершена ли асинхронная операция, получения вычисленного результата и т. д.

Более того, APIcancel(boolean mayInterruptIfRunning) отменяет операцию и освобождает выполняющийся поток. Если значениеmayInterruptIfRunning истинно, поток, выполняющий задачу, будет немедленно завершен.

В противном случае выполняемые задачи будут разрешены для выполнения.

Мы можем использовать приведенный ниже фрагмент кода для создания будущего экземпляра:

public void invoke() {
    ExecutorService executorService = Executors.newFixedThreadPool(10);

    Future future = executorService.submit(() -> {
        // ...
        Thread.sleep(10000l);
        return "Hello world";
    });
}

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

if (future.isDone() && !future.isCancelled()) {
    try {
        str = future.get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

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

try {
    future.get(10, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
    e.printStackTrace();
}

2.5. CountDownLatchс

CountDownLatch (введено вJDK 5) - служебный класс, который блокирует набор потоков до завершения некоторой операции.

CountDownLatch инициализируется типомcounter(Integer); этот счетчик уменьшается по мере завершения выполнения зависимых потоков. Но как только счетчик достигает нуля, другие потоки освобождаются.

Вы можете узнать больше оCountDownLatchhere.

2.6. CyclicBarrierс

CyclicBarrier работает почти так же, какCountDownLatch, за исключением того, что мы можем использовать его повторно. В отличие отCountDownLatch, он позволяет нескольким потокам ждать друг друга, используя методawait() (известный как барьерное условие) перед вызовом финальной задачи.

Нам нужно создать экземпляр задачиRunnable для инициации барьерного условия:

public class Task implements Runnable {

    private CyclicBarrier barrier;

    public Task(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    @Override
    public void run() {
        try {
            LOG.info(Thread.currentThread().getName() +
              " is waiting");
            barrier.await();
            LOG.info(Thread.currentThread().getName() +
              " is released");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

}

Теперь мы можем вызывать некоторые потоки, чтобы бороться за условие барьера:

public void start() {

    CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
        // ...
        LOG.info("All previous tasks are completed");
    });

    Thread t1 = new Thread(new Task(cyclicBarrier), "T1");
    Thread t2 = new Thread(new Task(cyclicBarrier), "T2");
    Thread t3 = new Thread(new Task(cyclicBarrier), "T3");

    if (!cyclicBarrier.isBroken()) {
        t1.start();
        t2.start();
        t3.start();
    }
}

Здесь методisBroken() проверяет, не прервался ли какой-либо из потоков во время выполнения. Мы должны всегда выполнять эту проверку перед выполнением фактического процесса.

2.7. Semaphoreс

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

Если разрешение недоступно (черезtryAcquire()), потоку не разрешается переходить в критическую секцию; однако, если разрешение имеется, доступ предоставляется, и счетчик разрешений уменьшается.

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

Мы можем указать тайм-аут для получения доступа с помощью методаtryAcquire(long timeout, TimeUnit unit).

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

Следующий фрагмент кода может быть использован для реализации семафора:

static Semaphore semaphore = new Semaphore(10);

public void execute() throws InterruptedException {

    LOG.info("Available permit : " + semaphore.availablePermits());
    LOG.info("Number of threads waiting to acquire: " +
      semaphore.getQueueLength());

    if (semaphore.tryAcquire()) {
        try {
            // ...
        }
        finally {
            semaphore.release();
        }
    }

}

Мы можем реализовать структуру данных, подобнуюMutex, используяSemaphore. Подробнее об этомcan be found here.

2.8. ThreadFactoryс

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

Мы можем определитьThreadFactory:

public class exampleThreadFactory implements ThreadFactory {
    private int threadId;
    private String name;

    public exampleThreadFactory(String name) {
        threadId = 1;
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, name + "-Thread_" + threadId);
        LOG.info("created new thread with id : " + threadId +
            " and name : " + t.getName());
        threadId++;
        return t;
    }
}

Мы можем использовать этот методnewThread(Runnable r) для создания нового потока во время выполнения:

exampleThreadFactory factory = new exampleThreadFactory(
    "exampleThreadFactory");
for (int i = 0; i < 10; i++) {
    Thread t = factory.newThread(new Task());
    t.start();
}

2.9. BlockingQueue

В асинхронном программировании одним из наиболее распространенных шаблонов интеграции являетсяproducer-consumer pattern. Пакетjava.util.concurrent поставляется со структурой данных, известной какBlockingQueue, которая может быть очень полезна в этих асинхронных сценариях.

Дополнительная информация и рабочий пример доступныhere.

2.10. DelayQueueс

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

Дополнительная информация и рабочий пример доступныhere.

2.11. Locksс

Неудивительно, чтоLock - это утилита для блокировки доступа других потоков к определенному сегменту кода, кроме потока, который его выполняет в данный момент.

Основное различие между блокировкой и синхронизированным блоком заключается в том, что синхронизированный блок полностью содержится в методе; однако мы можем использовать операции Lock () и unlock () API-интерфейса блокировки в отдельных методах.

Дополнительная информация и рабочий пример доступныhere.

2.12. Phaserс

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

Дополнительная информация и рабочий пример доступныhere.

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

В этой обзорной статье высокого уровня мы сосредоточились на различных утилитах, доступных в пакетеjava.util.concurrent.

Как всегда, доступен полный исходный кодover on GitHub.