1. введение
В этом уроке мы рассмотрим различные способы запуска потока и выполнения параллельных задач.
Это очень полезно, в частности , при работе с длительными или повторяющимися операциями, которые не могут выполняться в основном потоке , или когда взаимодействие с пользовательским интерфейсом не может быть приостановлено во время ожидания результатов операции.
Чтобы узнать больше о деталях потоков, обязательно прочитайте наш учебник о жизненном цикле потока в Java.
2. Основы запуска потока
Мы можем легко написать некоторую логику, которая работает в параллельном потоке, используя фреймворк Thread .
Давайте попробуем простой пример, расширив класс Thread :
public class NewThread extends Thread { public void run() { long startTime = System.currentTimeMillis(); int i = 0; while (true) { System.out.println(this.getName() + ": New Thread is running..." + i++); try { //Wait for one sec so it doesn't print too fast Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ... } } }
А теперь мы пишем второй класс для инициализации и запуска нашего потока:
public class SingleThreadExample { public static void main(String[] args) { NewThread t = new NewThread(); t.start(); } }
Мы должны вызвать метод start() для потоков в состоянии NEW |/(эквивалент не запущен). В противном случае Java выдаст экземпляр IllegalThreadStateException исключение.
Теперь предположим, что нам нужно запустить несколько потоков:
public class MultipleThreadsExample { public static void main(String[] args) { NewThread t1 = new NewThread(); t1.setName("MyThread-1"); NewThread t2 = new NewThread(); t2.setName("MyThread-2"); t1.start(); t2.start(); } }
Наш код по-прежнему выглядит довольно простым и очень похожим на примеры, которые мы можем найти в Интернете.
Конечно, это далеко не готовый к производству код, где крайне важно правильно управлять ресурсами, чтобы избежать слишком большого переключения контекста или слишком большого использования памяти.
Итак, чтобы подготовиться к производству, нам теперь нужно написать дополнительный шаблон для работы с:
- последовательное создание новых потоков
- количество параллельных живых потоков
- освобождение потоков: очень важно для потоков демона, чтобы избежать утечек
Если мы захотим, мы можем написать ваш собственный код для всех этих сценариев и даже для некоторых других, но зачем нам изобретать велосипед?
3. Фреймворк ExecutorService
ExecutorService реализует шаблон проектирования пула потоков (также называемый реплицированной моделью рабочего или рабочей группы) и заботится об управлении потоками, о котором мы упоминали выше, а также добавляет некоторые очень полезные функции, такие как возможность повторного использования потоков и очереди задач.
В частности, очень важна возможность повторного использования потоков: в крупномасштабном приложении выделение и освобождение многих объектов потоков создает значительные накладные расходы на управление памятью.
С рабочими потоками мы минимизируем накладные расходы, вызванные созданием потоков.
Чтобы упростить настройку пула, ExecutorService поставляется с простым конструктором и некоторыми параметрами настройки, такими как тип очереди, минимальное и максимальное количество потоков и соглашение об их именовании.
Для получения более подробной информации о ExecutorService, пожалуйста, прочитайте наше Руководство по Java ExecutorService .
4. Запуск задачи с исполнителями
Благодаря этой мощной структуре мы можем переключить наше мышление с запуска потоков на отправку задач.
Давайте посмотрим, как мы можем отправить асинхронную задачу нашему исполнителю:
ExecutorService executor = Executors.newFixedThreadPool(10); ... executor.submit(() -> { new Task(); });
Есть два метода , которые мы можем использовать: execute , который ничего не возвращает , и submit , который возвращает Future , инкапсулирующий результат вычисления.
Для получения дополнительной информации о фьючерсах, пожалуйста, прочитайте наше Руководство по java.util.concurrent.Будущее .
5. Запуск задачи с помощью Completablefuture
Для получения конечного результата из объекта Future мы можем использовать метод get , доступный в объекте, но это заблокирует родительский поток до конца вычисления.
В качестве альтернативы, мы могли бы избежать блокировки, добавив больше логики в нашу задачу, но мы должны увеличить сложность нашего кода.
Java 1.8 представила новый фреймворк поверх Будущее конструкция для лучшей работы с результатом вычисления: CompletableFuture .
CompletableFuture инвентарь Завершаемый Этап , который добавляет широкий выбор методов для присоединения обратных вызовов и избегает всех процедур, необходимых для выполнения операций с результатом после его готовности.
Реализация для отправки задачи намного проще:
CompletableFuture.supplyAsync(() -> "Hello");
supplyAsync принимает Supplier , содержащий код, который мы хотим выполнить асинхронно — в нашем случае параметр lambda.
Теперь задача неявно передается в ForkJoinPool.commonPool() , или мы можем указать Исполнитель мы предпочитаем в качестве второго параметра.
Чтобы узнать больше о CompletableFuture, пожалуйста, прочитайте наше Руководство по CompletableFuture .
6. Выполнение отложенных или периодических задач
При работе со сложными веб-приложениями нам может потребоваться выполнять задачи в определенное время, может быть, регулярно.
В Java есть несколько инструментов, которые могут помочь нам выполнять отложенные или повторяющиеся операции:
- В Java есть несколько инструментов, которые могут помочь нам выполнять отложенные или повторяющиеся операции:
- В Java есть несколько инструментов, которые могут помочь нам выполнять отложенные или повторяющиеся операции:
6.1. Таймер
Таймер – это средство для планирования задач для будущего выполнения в фоновом потоке.
Задачи могут быть запланированы для однократного выполнения или для повторного выполнения через регулярные промежутки времени.
Давайте посмотрим, как выглядит код, если мы хотим запустить задачу после одной секунды задержки:
TimerTask task = new TimerTask() { public void run() { System.out.println("Task performed on: " + new Date() + "n" + "Thread's name: " + Thread.currentThread().getName()); } }; Timer timer = new Timer("Timer"); long delay = 1000L; timer.schedule(task, delay);
Теперь давайте добавим повторяющееся расписание:
timer.scheduleAtFixedRate(repeatedTask, delay, period);
На этот раз задача будет выполняться после указанной задержки и будет повторяться по прошествии определенного периода времени.
Для получения дополнительной информации, пожалуйста, прочитайте наше руководство по Java Timer .
6.2. ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor имеет методы, аналогичные классу Timer :
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); ScheduledFuture
Чтобы закончить наш пример, мы используем scheduleAtFixedRate() для повторяющихся задач:
ScheduledFuture
Приведенный выше код выполнит задачу с начальной задержкой в 100 миллисекунд, а после этого он будет выполнять одну и ту же задачу каждые 450 миллисекунд.
Если процессор не может завершить обработку задачи вовремя до следующего вхождения, ScheduledExecutorService будет ждать, пока текущая задача не будет завершена, прежде чем приступить к следующей.
Чтобы избежать этого времени ожидания, мы можем использовать scheduleWithFixedDelay() , который, как описано в его названии, гарантирует задержку фиксированной длины между итерациями задачи.
6.3. Какой Инструмент Лучше?
Если мы запустим примеры выше, результат вычисления будет выглядеть так же.
Итак, как нам выбрать правильный инструмент ?
Когда фреймворк предлагает несколько вариантов, важно понять лежащую в его основе технологию, чтобы принять обоснованное решение.
Давайте попробуем нырнуть немного глубже под капот.
Таймер :
- не предоставляет гарантий в режиме реального времени: он планирует задачи с помощью метода Object.wait(long)
- существует один фоновый поток, поэтому задачи выполняются последовательно, и длительная задача может задержать другие
- исключения во время выполнения, создаваемые в TimerTask убьет единственный доступный поток, тем самым убив Таймер
ScheduledThreadPoolExecutor :
- может быть сконфигурирован с любым количеством потоков
- может воспользоваться всеми доступными ядрами процессора
- улавливает исключения во время выполнения и позволяет нам обрабатывать их, если мы хотим (переопределяя метод afterExecute из ThreadPoolExecutor )
- отменяет задачу, вызвавшую исключение, позволяя другим продолжать выполнение
- полагается на систему планирования ОС для отслеживания часовых поясов, задержек, солнечного времени и т. Д.
- предоставляет API для совместной работы, если нам нужна координация между несколькими задачами, например, ожидание завершения всех отправленных задач
- обеспечивает лучший API для управления жизненным циклом потока
Выбор сейчас очевиден, не так ли?
7. Разница между Будущим и Запланированным будущим
В наших примерах кода мы можем наблюдать, что ScheduledThreadPoolExecutor возвращает определенный тип Будущее : Запланированное будущее .
ScheduledFuture расширяет оба интерфейса Future и Delayed , таким образом наследуя дополнительный метод getDelay , который возвращает оставшуюся задержку, связанную с текущей задачей. Он расширен с помощью RunnableScheduledFuture , который добавляет метод для проверки, является ли задача периодической.
ScheduledThreadPoolExecutor реализует все эти конструкции через внутренний класс ScheduledFutureTask и использует их для управления жизненным циклом задачи.
8. Выводы
В этом уроке мы экспериментировали с различными фреймворками, доступными для запуска потоков и параллельного выполнения задач.
Затем мы углубились в различия между Timer и ScheduledThreadPoolExecutor.
Исходный код статьи доступен на GitHub .