Рубрики
Без рубрики

Параллелизм в Java: Платформа исполнителя

Автор оригинала: Chandan Singh.

Вступление

С увеличением числа ядер, доступных в процессорах в настоящее время, в сочетании с постоянно растущей потребностью в повышении пропускной способности, многопоточные API становятся довольно популярными. Java предоставляет свою собственную многопоточную платформу, называемую Executor Framework .

Что такое Структура исполнителя?

Платформа исполнителя содержит набор компонентов, которые используются для эффективного управления рабочими потоками. API исполнителя отделяет выполнение задачи от реальной задачи, которая должна быть выполнена через Исполнителей . Этот дизайн является одной из реализаций шаблона Производитель-потребитель .

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

Чтобы использовать платформу исполнителя, нам нужно создать один такой пул потоков и отправить ему задачу для выполнения. Задача платформы исполнителя – планировать и выполнять отправленные задачи, а также возвращать результаты из пула потоков.

Основной вопрос, который приходит на ум, заключается в том, зачем нам нужны такие пулы потоков, когда мы можем создавать объекты java.lang.Поток или реализовать Управляемые /| Вызываемые интерфейсы для достижения параллелизма?

Ответ сводится к двум основным фактам:

  1. Создание нового потока для новой задачи приводит к накладным расходам на создание и разрыв потока. Управление жизненным циклом этого потока значительно увеличивает время выполнения.
  2. Добавление нового потока для каждого процесса без какого-либо регулирования приводит к созданию большого количества потоков. Эти потоки занимают память и приводят к потере ресурсов. Процессор начинает тратить слишком много времени на переключение контекстов, когда каждый поток заменяется и для выполнения запускается другой поток.

Все эти факторы снижают пропускную способность системы. Пулы потоков устраняют эту проблему, сохраняя потоки живыми и повторно используя потоки. Любые избыточные задачи, поступающие в поток, с которыми могут справиться потоки в пуле, хранятся в очереди /. Как только какой-либо из потоков освобождается, они выбирают следующую задачу из этой очереди. Эта очередь задач по существу неограничена для готовых исполнителей, предоставляемых JDK.

Типы исполнителей

Теперь, когда у нас есть хорошее представление о том, что такое исполнитель, давайте также рассмотрим различные виды исполнителей.

Однопоточный исполнитель

Этот исполнитель пула потоков имеет только один поток. Он используется для последовательного выполнения задач. Если поток умирает из-за исключения во время выполнения задачи, создается новый поток для замены старого потока, и последующие задачи выполняются в новом потоке.

ExecutorService executorService = Executors.newSingleThreadExecutor()

Фиксированный пул потоков(n)

Как следует из названия, это пул потоков с фиксированным количеством потоков. Задачи, отправленные исполнителю, выполняются потоками n , и если есть еще задачи, они сохраняются в LinkedBlockingQueue . Это число обычно представляет собой общее количество потоков, поддерживаемых базовым процессором.

ExecutorService executorService = Executors.newFixedThreadPool(4);

Кэшированный пул потоков

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

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

ExecutorService executorService = Executors.newCachedThreadPool();

Запланированный Исполнитель

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

ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);

Задачи могут быть запланированы в Запланированном исполнителе с использованием любого из двух методов scheduleAtFixedRate или |/scheduleWithFixedDelay .

scheduledExecService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
scheduledExecService.scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)

Основное различие между этими двумя методами заключается в их интерпретации задержки между последовательными выполнением запланированного задания.

scheduleAtFixedRate выполняет задачу с фиксированным интервалом, независимо от того, когда завершилась предыдущая задача.

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

Понимание будущего объекта

К результату задачи, отправленной на выполнение исполнителю, можно получить доступ с помощью java.util.concurrent.Будущее объект, возвращенный исполнителем. Будущее можно рассматривать как обещание, данное вызывающему абоненту исполнителем.

Future result = executorService.submit(callableTask);

Git Essentials

Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!

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

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

Если вызывающий абонент не может позволить себе ждать бесконечно долго, прежде чем получить результат, это ожидание также может быть рассчитано по времени. Это достигается с помощью метода Future.get(длительный тайм-аут, единица измерения времени) , который выдает Исключение TimeoutException если результат не будет возвращен в оговоренные сроки. Вызывающий может обработать это исключение и продолжить дальнейшее выполнение программы.

Если при выполнении задачи возникает исключение, вызов метода get вызовет исключение ExecutionException .

Важным моментом в отношении результата, возвращаемого методом Future.get () , является то, что он возвращается только в том случае, если отправленная задача реализует java.util.concurrent.Вызываемый . Если задача реализует Выполняемый интерфейс, вызов . get() вернет null после завершения задачи.

Другим важным методом является метод Future.cancel(логическое значение mayInterruptIfRunning) . Этот метод используется для отмены выполнения отправленной задачи. Если задача уже выполняется, исполнитель попытается прервать выполнение задачи, если флаг mayInterruptIfRunning передан как true .

Пример: Создание и выполнение простого исполнителя

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

public class Task implements Callable {

    private String message;

    public Task(String message) {
        this.message = message;
    }

    @Override
    public String call() throws Exception {
        return "Hello " + message + "!";
    }
}

Класс Задача реализует Вызываемый и параметризуется в Строковый тип. Также объявлено, что он выбрасывает Исключение . Эта возможность выдавать исключение исполнителю, а исполнитель возвращает это исключение обратно вызывающему абоненту, имеет большое значение, поскольку она помогает вызывающему абоненту узнать статус выполнения задачи.

Теперь давайте выполним эту задачу:

public class ExecutorExample {
    public static void main(String[] args) {

        Task task = new Task("World");

        ExecutorService executorService = Executors.newFixedThreadPool(4);
        Future result = executorService.submit(task);

        try {
            System.out.println(result.get());
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("Error occured while executing the submitted task");
            e.printStackTrace();
        }

        executorService.shutdown();
    }
}

Здесь мы создали FixedThreadPool исполнитель с количеством потоков 4, так как эта демонстрационная версия разработана на четырехъядерном процессоре. Количество потоков может превышать количество ядер процессора, если выполняемые задачи выполняют значительные операции ввода-вывода или тратят время на ожидание внешних ресурсов.

Мы создали экземпляр класса Task и передаем его исполнителю для выполнения. Результат возвращается объектом Future , который мы затем выводим на экран.

Давайте запустим пример Исполнителя и проверим его вывод:

Hello World!

Как и ожидалось, задача добавляет приветствие “Привет” и возвращает результат через объект Future .

Наконец, мы вызываем завершение работы объекта ExecutorService , чтобы завершить все потоки и вернуть ресурсы обратно в ОС.

Метод .shutdown() ожидает завершения текущих задач, переданных исполнителю. Однако, если требуется немедленно закрыть исполнителя, не дожидаясь, то вместо этого мы можем использовать метод .shutdownNow () .

Все задачи, ожидающие выполнения, будут возвращены обратно в java.util.Список объект.

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

public class Task implements Runnable{

    private String message;

    public Task(String message) {
        this.message = message;
    }

    public void run() {
        System.out.println("Hello " + message + "!");
    }
}

Здесь есть несколько важных изменений, когда мы внедряем runnable.

  1. Результат выполнения задачи не может быть возвращен из метода run () . Следовательно, мы печатаем прямо отсюда.
  2. Метод run() не настроен для создания каких-либо проверенных исключений.

Вывод

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

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

Как всегда, код для этого примера можно найти на GitHub .